TensorFlow-机器学习项目-全-

TensorFlow 机器学习项目(全)

原文:annas-archive.org/md5/83fecd7b232ff9aa6762c486005b5094

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

TensorFlow 已经彻底改变了人们对机器学习的认知。TensorFlow 机器学习项目将教您如何利用 TensorFlow 在各种实际项目中发挥其优势——简洁性、效率和灵活性。在本书的帮助下,您不仅将学习如何使用不同的数据集构建高级项目,还能通过 TensorFlow 生态系统中的多个库解决常见的挑战。

首先,您将掌握使用 TensorFlow 进行机器学习项目的基本方法。您将通过使用 TensorForest 和 TensorBoard 进行系外行星检测、使用 TensorFlow.js 进行情感分析以及使用 TensorFlow Lite 进行数字分类,来探索一系列项目。

在阅读本书的过程中,您将构建涉及自然语言处理NLP)、高斯过程、自编码器、推荐系统和贝叶斯神经网络等多个实际领域的项目,还将涉及诸如生成对抗网络GANs)、胶囊网络和强化学习等热门领域。您将学习如何结合 Spark API 使用 TensorFlow,并探索利用 TensorFlow 进行 GPU 加速计算以进行物体检测,接着了解如何训练和开发递归神经网络RNN)模型来生成书籍脚本。

在本书结束时,您将获得足够的专业知识,能够在工作中构建完善的机器学习项目。

本书适合谁阅读

如果您是数据分析师、数据科学家、机器学习专家或具有基本 TensorFlow 知识的深度学习爱好者,那么《TensorFlow 机器学习项目》非常适合您。如果您想在机器学习领域使用监督学习、无监督学习和强化学习技术构建端到端的项目,这本书同样适合您。

本书涵盖的内容

第一章,TensorFlow 与机器学习概述,解释了 TensorFlow 的基本概念,并让您构建一个使用逻辑回归分类手写数字的机器学习模型。

第二章,使用机器学习检测外太空中的系外行星,介绍了如何使用基于决策树的集成方法来检测外太空中的系外行星。

第三章,在浏览器中使用 TensorFlow.js 进行情感分析,介绍了如何在您的网页浏览器上使用 TensorFlow.js 训练和构建模型。我们将使用电影评论数据集构建一个情感分析模型,并将其部署到浏览器中进行预测。

第四章,使用 TensorFlow Lite 进行数字分类,重点介绍了构建深度学习模型来分类手写数字,并将其转换为适合移动设备的格式,使用 TensorFlow Lite。我们还将学习 TensorFlow Lite 的架构以及如何使用 TensorBoard 来可视化神经网络。

第五章,使用 NLP 进行语音转文本和主题提取,重点介绍了通过 TensorFlow 学习各种语音转文本选项以及 Google 在 TensorFlow 中提供的预构建模型,使用 Google 语音命令数据集。

第六章,使用高斯过程回归预测股票价格,解释了一种流行的预测模型——贝叶斯统计中的高斯过程。我们使用GpFlow库中构建的高斯过程,它是基于 TensorFlow 开发的,来开发股票价格预测模型。

第七章,使用自编码器进行信用卡欺诈检测,介绍了一种名为自编码器的降维技术。我们通过使用 TensorFlow 和 Keras 构建自编码器,识别信用卡数据集中的欺诈交易。

第八章,使用贝叶斯神经网络生成交通标志分类器的不确定性,解释了贝叶斯神经网络,它帮助我们量化预测中的不确定性。我们将使用 TensorFlow 构建贝叶斯神经网络,以对德国交通标志进行分类。

第九章,使用 DiscoGAN 从鞋子图像生成匹配的鞋袋,介绍了一种新的 GAN 类型——发现 GAN(DiscoGANs)。我们了解了它的架构与标准 GAN 的区别,以及它如何应用于风格迁移问题。最后,我们在 TensorFlow 中构建了一个 DiscoGAN 模型,从鞋子图像生成匹配的鞋袋,反之亦然。

第十章,使用胶囊网络对服装图像进行分类,实现了一个非常新的图像分类模型——胶囊网络。我们将了解它的架构,并解释在 TensorFlow 中实现时的细节。我们使用 Fashion MNIST 数据集,通过该模型对服装图像进行分类。

第十一章,使用 TensorFlow 进行优质产品推荐,介绍了诸如矩阵分解(SVD++)、学习排序以及用于推荐任务的卷积神经网络变体等技术。

第十二章,使用 TensorFlow 进行大规模目标检测,探索了 Yahoo 的TensorFlowOnSpark框架,用于在 Spark 集群上进行分布式深度学习。然后,我们将TensorFlowOnSpark应用于大规模图像数据集,并训练网络进行目标检测。

第十三章,使用 LSTM 生成书籍脚本,解释了 LSTM 在生成新文本方面的用途。我们使用 Packt 出版的书籍中的书籍脚本,构建了一个基于 LSTM 的深度学习模型,能够自动生成书籍脚本。

第十四章,使用深度强化学习玩吃豆人,解释了如何利用强化学习训练模型玩吃豆人,并在此过程中教你强化学习的相关知识。

第十五章,接下来是什么?,介绍了 TensorFlow 生态系统中的其他组件,这些组件对于在生产环境中部署模型非常有用。我们还将学习 AI 在各个行业中的应用,深度学习的局限性,以及 AI 中的伦理问题。

为了充分利用本书

为了充分利用本书,请从 GitHub 仓库下载本书代码,并在 Jupyter Notebooks 中练习代码。同时,练习修改作者已经提供的实现。

下载示例代码文件

你可以从你的账户在www.packt.com下载本书的示例代码文件。如果你是从其他地方购买的本书,可以访问www.packt.com/support并注册,直接将文件通过电子邮件发送给你。

你可以按照以下步骤下载代码文件:

  1. www.packt.com登录或注册。

  2. 选择 SUPPORT 标签。

  3. 点击代码下载和勘误。

  4. 在搜索框中输入书名,并按照屏幕上的指示操作。

下载文件后,请确保使用以下最新版本解压或提取文件夹:

  • 适用于 Windows 的 WinRAR/7-Zip

  • 适用于 Mac 的 Zipeg/iZip/UnRarX

  • 适用于 Linux 的 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上,网址为github.com/PacktPublishing/TensorFlow-Machine-Learning-Projects。如果代码有更新,它将被更新到现有的 GitHub 仓库中。

我们还提供了来自丰富书籍和视频目录的其他代码包,访问网址为github.com/PacktPublishing/。快来查看吧!

下载彩色图片

我们还提供了一份 PDF 文件,包含本书中使用的屏幕截图/图表的彩色图片。你可以在这里下载:www.packtpub.com/sites/default/files/downloads/9781789132212_ColorImages.pdf

使用的约定

本书中使用了多种文本约定。

CodeInText:表示文本中的代码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 账号。以下是一个示例:“通过定义占位符并将值传递给 session.run()。”

一段代码设置如下:

tf.constant(
  value,
  dtype=None,
  shape=None,
  name='const_name',
  verify_shape=False
  )

任何命令行输入或输出如下所示:

const1 (x):  Tensor("x:0", shape=(), dtype=int32)
const2 (y):  Tensor("y:0", shape=(), dtype=float32)
const3 (z):  Tensor("z:0", shape=(), dtype=float16)

粗体:表示新术语、重要单词或屏幕上出现的文字。例如,菜单或对话框中的词语在文本中通常是这样的格式。以下是一个示例:“在提供的框中输入评论并点击 提交 以查看模型预测的分数。”

警告或重要提示通常这样显示。

小贴士和技巧通常这样呈现。

获取联系方式

我们欢迎读者的反馈。

一般反馈:如果你对本书的任何部分有疑问,请在邮件主题中提及书名,并通过电子邮件联系我们,邮箱地址是 customercare@packtpub.com

勘误:虽然我们已尽最大努力确保内容的准确性,但错误偶尔会发生。如果你在本书中发现错误,我们将非常感激你能向我们报告。请访问 www.packt.com/submit-errata,选择你的书籍,点击“勘误提交表格”链接,并输入相关细节。

盗版:如果你在互联网上发现我们作品的任何非法复制品,请提供相关位置地址或网站名称。请通过 copyright@packt.com 联系我们,并附上链接。

如果你有兴趣成为作者:如果你在某个领域有专业知识,并且有兴趣写书或为书籍做贡献,请访问 authors.packtpub.com

评论

请留下评论。当你阅读并使用完本书后,为什么不在你购买书籍的网站上留下评论呢?潜在读者可以参考你的意见来做出购买决策,我们也可以了解你对我们产品的看法,而作者们也能看到你对他们书籍的反馈。谢谢!

想了解更多关于 Packt 的信息,请访问 packt.com

第一章:TensorFlow 和机器学习概述

TensorFlow 是一个流行的库,用于实现基于机器学习的解决方案。它包括一个低级 API,称为 TensorFlow 核心,以及许多高级 API,其中两个最受欢迎的 API 被称为 TensorFlow Estimators 和 Keras。在本章中,我们将学习 TensorFlow 的基础知识,并使用逻辑回归构建一个机器学习模型,以手写数字分类为例。

本章将涵盖以下内容:

  • TensorFlow 核心:

    • TensorFlow 核心中的张量

    • 常量

    • 占位符

    • 操作

    • 来自 Python 对象的张量

    • 变量

    • 来自库函数的张量

  • 计算图:

    • 延迟加载和执行顺序

    • 多设备上的图 – CPU 和 GPGPU

    • 使用多个图

  • 机器学习、分类和逻辑回归

  • TensorFlow 中的逻辑回归示例

  • Keras 中的逻辑回归示例

你可以通过使用名为 ch-01_Overview_of_TensorFlow_and_Machine_Learning.ipynb 的 Jupyter Notebook 来跟随本章中的代码示例,该 Notebook 已包含在代码包中。

什么是 TensorFlow?

TensorFlow 是一个流行的开源库,广泛用于实现机器学习和深度学习。它最初在 Google 内部构建,后来于 2015 年 11 月 9 日公开发布。从那时起,TensorFlow 被广泛用于开发多个业务领域的机器学习和深度学习模型。

要在我们的项目中使用 TensorFlow,我们需要学习如何使用 TensorFlow API 编程。TensorFlow 有多个 API 可供与库进行交互。TensorFlow 的 API 分为两级:

  • 低级 API:称为 TensorFlow 核心的 API 提供了细粒度的低级功能。因此,这个低级 API 在使用模型时提供了完全控制。我们将在本章中讨论 TensorFlow 核心。

  • 高级 API:这些 API 提供了建立在 TensorFlow 核心上的高级功能,比较容易学习和实现。一些高级 API 包括 Estimators、Keras、TFLearn、TFSlim 和 Sonnet。本章中我们也将讨论 Keras。

TensorFlow 核心

TensorFlow 核心 是构建更高层 TensorFlow 模块的低级 API。在本节中,我们将简要回顾 TensorFlow 核心,并了解 TensorFlow 的基本元素。

张量

张量 是 TensorFlow 中的基本组件。张量是一个多维数据元素集合。它通常由形状、类型和秩来标识。 指的是张量的维度数,而 形状 指的是每个维度的大小。你可能之前见过一些张量的例子,比如零维集合(也称为标量)、一维集合(也称为向量)和二维集合(也称为矩阵)。

标量值是一个秩为 0 且形状为[]的张量。向量或一维数组是一个秩为 1 且形状为[列数]或[行数]的张量。矩阵或二维数组是一个秩为 2 且形状为[行数, 列数]的张量。三维数组是一个秩为 3 的张量。以此类推,n 维数组是一个秩为 n 的张量。

一个张量可以在其所有维度中存储相同类型的数据,且张量的数据类型与其元素的数据类型相同。

可以在 TensorFlow 库中找到的数据类型在以下链接中有所描述:www.tensorflow.org/api_docs/python/tf/DType

以下是 TensorFlow 中最常用的数据类型:

TensorFlow Python API 数据类型 描述
tf.float16 16 位浮点数(半精度)
tf.float32 32 位浮点数(单精度)
tf.float64 64 位浮点数(双精度)
tf.int8 8 位整数(有符号)
tf.int16 16 位整数(有符号)
tf.int32 32 位整数(有符号)
tf.int64 64 位整数(有符号)

使用 TensorFlow 数据类型来定义张量,而不是使用 Python 原生数据类型或 NumPy 的数据类型。

张量可以通过以下几种方式创建:

  • 通过定义常量、操作和变量,并将值传递给它们的构造函数

  • 通过定义占位符并将值传递给session.run()

  • 通过将 Python 对象(如标量值、列表、NumPy 数组和 pandas DataFrame)转换为张量,使用tf.convert_to_tensor()函数

让我们探索不同的创建张量的方式。

常量

常量值张量是通过tf.constant()函数创建的,具有以下定义:

tf.constant(
  value,
  dtype=None,
  shape=None,
  name='const_name',
  verify_shape=False
  )

让我们用以下代码创建一些常量:

const1=tf.constant(34,name='x1')
const2=tf.constant(59.0,name='y1')
const3=tf.constant(32.0,dtype=tf.float16,name='z1')

让我们详细看看前面的代码:

  • 第一行代码定义了一个常量张量const1,存储值34,并命名为x1

  • 第二行代码定义了一个常量张量const2,存储了值59.0,并命名为y1

  • 第三行代码为const3定义了数据类型tf.float16。使用dtype参数或将数据类型作为第二个参数来指定数据类型。

让我们打印常量const1const2const3

print('const1 (x): ',const1)
print('const2 (y): ',const2)
print('const3 (z): ',const3)

当我们打印这些常量时,我们将得到以下输出:

const1 (x):  Tensor("x:0", shape=(), dtype=int32)
const2 (y):  Tensor("y:0", shape=(), dtype=float32)
const3 (z):  Tensor("z:0", shape=(), dtype=float16)

打印之前定义的张量时,我们可以看到const1const2的数据类型由 TensorFlow 自动推断。

要打印这些常量的值,我们可以在 TensorFlow 会话中执行它们,使用tfs.run()命令:

print('run([const1,const2,c3]) : ',tfs.run([const1,const2,const3]))

我们将看到以下输出:

run([const1,const2,const3]) : [34, 59.0, 32.0]

操作

TensorFlow 库包含几个内置的操作,可以应用于张量。操作节点可以通过传递输入值并将输出保存在另一个张量中来定义。为了更好地理解这一点,我们来定义两个操作,op1op2

op1 = tf.add(const2, const3)
op2 = tf.multiply(const2, const3)

让我们打印 op1op2

print('op1 : ', op1)
print('op2 : ', op2)

输出如下,显示 op1op2 被定义为张量:

op1 :  Tensor("Add:0", shape=(), dtype=float32)
op2 :  Tensor("Mul:0", shape=(), dtype=float32)

要打印执行这些操作后的输出,必须在 TensorFlow 会话中执行 op1op2 张量:

print('run(op1) : ', tfs.run(op1))
print('run(op2) : ', tfs.run(op2))

输出如下:

run(op1) :  91.0
run(op2) :  1888.0

TensorFlow 的一些内置操作包括算术运算、数学函数和复数运算。

占位符

虽然常量在定义张量时存储值,占位符允许你创建空张量,以便在运行时提供值。TensorFlow 库提供了一个名为 tf.placeholder() 的函数,以下是它的签名,用于创建占位符:

tf.placeholder(
  dtype,
  shape=None,
  name=None
  )

作为一个示例,我们来创建两个占位符并打印它们:

p1 = tf.placeholder(tf.float32)
p2 = tf.placeholder(tf.float32)
print('p1 : ', p1)
print('p2 : ', p2)

以下输出显示每个占位符已被创建为一个张量:

p1 :  Tensor("Placeholder:0", dtype=float32)
p2 :  Tensor("Placeholder_1:0", dtype=float32)

让我们使用这些占位符定义一个操作:

mult_op = p1 * p2

在 TensorFlow 中,可以使用简写符号进行各种操作。在前面的代码中,p1 * p2tf.multiply(p1, p2) 的简写:

print('run(mult_op,{p1:13.4, p2:61.7}) : ',tfs.run(mult_op,{p1:13.4, p2:61.7}))

上述命令在 TensorFlow 会话中运行 mult_op,并通过值字典(run() 操作的第二个参数)为 p1p2 提供值。

输出如下:

run(mult_op,{p1:13.4, p2:61.7}) :  826.77997

我们还可以通过在 run() 操作中使用 feed_dict 参数来指定值字典:

feed_dict={p1: 15.4, p2: 19.5}
print('run(mult_op,feed_dict = {p1:15.4, p2:19.5}) : ',
      tfs.run(mult_op, feed_dict=feed_dict))

输出如下:

run(mult_op,feed_dict = {p1:15.4, p2:19.5}) :  300.3

让我们看一个最终的示例,展示一个向量被传递到同一个操作中的情况:

feed_dict={p1: [2.0, 3.0, 4.0], p2: [3.0, 4.0, 5.0]}
print('run(mult_op,feed_dict={p1:[2.0,3.0,4.0], p2:[3.0,4.0,5.0]}):',
      tfs.run(mult_op, feed_dict=feed_dict))

输出如下:

run(mult_op,feed_dict={p1:[2.0,3.0,4.0],p2:[3.0,4.0,5.0]}):[  6\.  12\.  20.]

两个输入向量的元素按元素逐一相乘。

从 Python 对象创建张量

张量可以通过 Python 对象(如列表、NumPy 数组和 pandas DataFrame)创建。要从 Python 对象创建张量,请使用 tf.convert_to_tensor() 函数,以下是它的定义:

tf.convert_to_tensor(
  value,
  dtype=None,
  name=None,
  preferred_dtype=None
  )

让我们通过创建一些张量并打印它们的定义和值来进行练习:

  1. 定义一个零维张量:
tf_t=tf.convert_to_tensor(5.0,dtype=tf.float64)

print('tf_t : ',tf_t)
print('run(tf_t) : ',tfs.run(tf_t))

输出如下:

tf_t : Tensor("Const_1:0", shape=(), dtype=float64)
run(tf_t) : 5.0
  1. 定义一个一维张量:
a1dim = np.array([1,2,3,4,5.99])
print("a1dim Shape : ",a1dim.shape)

tf_t=tf.convert_to_tensor(a1dim,dtype=tf.float64)

print('tf_t : ',tf_t)
print('tf_t[0] : ',tf_t[0])
print('tf_t[0] : ',tf_t[2])
print('run(tf_t) : \n',tfs.run(tf_t))

输出如下:

a1dim Shape :  (5,)
tf_t :  Tensor("Const_2:0", shape=(5,), dtype=float64)
tf_t[0] :  Tensor("strided_slice:0", shape=(), dtype=float64)
tf_t[0] :  Tensor("strided_slice_1:0", shape=(), dtype=float64)
run(tf_t) : 
 [ 1\.    2\.    3\.    4\.    5.99]
  1. 定义一个二维张量:
a2dim = np.array([(1,2,3,4,5.99),
                  (2,3,4,5,6.99),
                  (3,4,5,6,7.99)
                 ])
print("a2dim Shape : ",a2dim.shape)

tf_t=tf.convert_to_tensor(a2dim,dtype=tf.float64)

print('tf_t : ',tf_t)
print('tf_t[0][0] : ',tf_t[0][0])
print('tf_t[1][2] : ',tf_t[1][2])
print('run(tf_t) : \n',tfs.run(tf_t))

输出如下:

a2dim Shape :  (3, 5)
tf_t :  Tensor("Const_3:0", shape=(3, 5), dtype=float64)
tf_t[0][0] :  Tensor("strided_slice_3:0", shape=(), dtype=float64)
tf_t[1][2] :  Tensor("strided_slice_5:0", shape=(), dtype=float64)
run(tf_t) : 
 [[ 1\.    2\.    3\.    4\.    5.99]
  [ 2\.    3\.    4\.    5\.    6.99]
  [ 3\.    4\.    5\.    6\.    7.99]]
  1. 定义一个三维张量:
a3dim = np.array([[[1,2],[3,4]],
                  [[5,6],[7,8]]
                 ])
print("a3dim Shape : ",a3dim.shape)

tf_t=tf.convert_to_tensor(a3dim,dtype=tf.float64)

print('tf_t : ',tf_t)
print('tf_t[0][0][0] : ',tf_t[0][0][0])
print('tf_t[1][1][1] : ',tf_t[1][1][1])
print('run(tf_t) : \n',tfs.run(tf_t))

输出如下:

a3dim Shape :  (2, 2, 2)
tf_t :  Tensor("Const_4:0", shape=(2, 2, 2), dtype=float64)
tf_t[0][0][0] :  Tensor("strided_slice_8:0", shape=(), dtype=float64)
tf_t[1][1][1] :  Tensor("strided_slice_11:0", shape=(), dtype=float64)
run(tf_t) : 
 [[[ 1\.  2.][ 3\.  4.]]
  [[ 5\.  6.][ 7\.  8.]]]

变量

在前面的章节中,我们学习了如何定义不同类型的张量对象,例如常量、操作和占位符。在使用 TensorFlow 构建和训练模型时,参数的值需要保存在可更新的内存位置。这些用于张量的可更新内存位置在 TensorFlow 中被称为变量。

总结一下,TensorFlow 变量是张量对象,因为它们的值可以在程序执行过程中被修改。

虽然 tf.Variable 看起来与 tf.placeholder 相似,但它们之间有一些差异。以下表格列出了这些差异:

tf.placeholder tf.Variable
tf.placeholder 定义了不随时间更新的输入数据 tf.Variable 定义了随时间更新的值
tf.placeholder 在定义时不需要提供初始值 tf.Variable 在定义时需要提供初始值

在 TensorFlow 中,变量可以通过 API 函数tf.Variable()创建。让我们看一个使用占位符和变量的例子,并在 TensorFlow 中创建以下模型:

  1. 将模型参数wb定义为变量,初始值分别为[.3][-0.3]
w = tf.Variable([.3], tf.float32)
b = tf.Variable([-.3], tf.float32)
  1. 定义输入占位符x和输出操作节点y
x = tf.placeholder(tf.float32)
y = w * x + b
  1. 打印变量和占位符wvxy
print("w:",w)
print("x:",x)
print("b:",b)
print("y:",y)

输出描绘了节点的类型,如VariablePlaceholder或操作节点,如下所示:

w: <tf.Variable 'Variable:0' shape=(1,) dtype=float32_ref>
x: Tensor("Placeholder_2:0", dtype=float32)
b: <tf.Variable 'Variable_1:0' shape=(1,) dtype=float32_ref>
y: Tensor("add:0", dtype=float32)

上述输出表明,x是一个Placeholder张量,y是一个操作张量,wb是具有形状(1,)和数据类型float32的变量。

在 TensorFlow 会话中,变量必须在使用之前进行初始化。我们可以通过运行其初始化操作来初始化单个变量,也可以初始化所有或一组变量。

例如,要初始化w变量,可以使用以下代码:

tfs.run(w.initializer)

TensorFlow 提供了一个方便的函数,可以初始化所有变量:

tfs.run(tf.global_variables_initializer())

TensorFlow 还提供了tf.variables_initializer()函数,可以初始化一组特定的变量。

初始化这些变量的全局便利函数可以以另一种方式执行。与在会话对象的run()函数内部执行不同,可以直接执行由初始化函数返回的对象的运行函数:

tf.global_variables_initializer().run()

在变量初始化后,执行模型以获取输入值x = [1,2,3,4]的输出:

print('run(y,{x:[1,2,3,4]}) : ',tfs.run(y,{x:[1,2,3,4]}))

输出如下:

run(y,{x:[1,2,3,4]}) :  [ 0\.          0.30000001  0.60000002  0.90000004]

从库函数生成的张量

TensorFlow 提供了各种函数来生成具有预填充值的张量。这些函数生成的值可以存储在常量或变量张量中,这些生成的值也可以在初始化时提供给张量构造函数。

作为示例,生成一个预先填充了100个零的 1 维张量:

a=tf.zeros((100,))
print(tfs.run(a))

TensorFlow 库中的一些函数会在定义时用不同的值填充这些张量,列举如下:

  • 用相同的值填充张量的所有元素:tf.ones_like()tf.ones()tf.fill()tf.zeros()tf.zeros_like()

  • 用序列填充张量:tf.range()tf.lin_space()

  • 用概率分布填充张量:tf.random_uniform()tf.random_normal()tf.random_gamma()tf.truncated_normal()

使用tf.get_variable()获取变量

如果一个变量的名称已被另一个变量使用,则 TensorFlow 会抛出异常。tf.get_variable()函数提供了一种方便且安全的方式来创建变量,取代使用tf.Variable()函数。tf.get_variable()函数返回一个已定义的变量,如果给定名称的变量不存在,则会使用指定的初始化器和形状创建该变量。

请考虑以下示例:

w = tf.get_variable(name='w',shape=[1],dtype=tf.float32,initializer=[.3])
b = tf.get_variable(name='b',shape=[1],dtype=tf.float32,initializer=[-.3])

初始化器可以是值的列表或另一个张量。初始化器还可以是内置初始化器之一,部分初始化器如下:

  • tf.ones_initializer

  • tf.constant_initializer

  • tf.zeros_initializer

  • tf.truncated_normal_initializer

  • tf.random_normal_initializer

  • tf.random_uniform_initializer

  • tf.uniform_unit_scaling_initializer

  • tf.orthogonal_initializer

tf.get_variable()函数仅在跨多个机器运行的分布式 TensorFlow 中返回全局变量。可以通过tf.get_local_variable()函数来检索本地变量。

共享或重用变量:获取已经定义的变量可以促进重用。然而,如果没有通过tf.variable_scope.reuse_variable()tf.variable.scope(reuse=True)设置重用标志,系统将抛出异常。

现在我们已经学习了如何定义张量、常量、操作、占位符和变量,接下来让我们学习 TensorFlow 中的更高级别的抽象,它将这些基本元素组合在一起形成计算的基本单元:计算图。

计算图

计算图是 TensorFlow 中计算的基本单元。计算图由节点和边组成。每个节点表示一个tf.Operation的实例,而每条边代表一个tf.Tensor的实例,数据在节点之间传输。

TensorFlow 中的模型包含计算图。首先,您必须创建包含表示变量、常量、占位符和操作的节点的图,然后将该图提供给 TensorFlow 执行引擎。TensorFlow 执行引擎会找到它可以执行的第一组节点。执行这些节点会启动后续节点的执行,遵循计算图的顺序。

因此,基于 TensorFlow 的程序由在计算图上执行的两种活动组成:

  • 定义计算图

  • 执行计算图

一个 TensorFlow 程序在默认图中开始执行。除非显式指定另一个图,否则新节点会隐式地添加到默认图中。可以使用以下命令显式访问默认图:

graph = tf.get_default_graph()

例如,以下计算图表示将三个输入相加以产生输出,即 

在 TensorFlow 中,前面示意图中的加法操作节点对应的代码是 y = tf.add( x1 + x2 + x3 )

变量、常量和占位符在创建时会被添加到图中。定义计算图后,会话对象被实例化,它会执行操作对象并评估张量对象。

让我们定义并执行一个计算图来计算 ,就像我们在前面的示例中看到的那样:

# Linear Model y = w * x + b
# Define the model parameters
w = tf.Variable([.3], tf.float32)
b = tf.Variable([-.3], tf.float32)
# Define model input and output
x = tf.placeholder(tf.float32)
y = w * x + b
output = 0

with tf.Session() as tfs:
   # initialize and print the variable y
   tf.global_variables_initializer().run()
   output = tfs.run(y,{x:[1,2,3,4]})
print('output : ',output)

with 块中创建并使用会话可以确保在块执行完成后会话自动关闭。否则,必须通过 tfs.close() 命令显式关闭会话,其中 tfs 是会话名称。

执行顺序与懒加载

计算图中的节点按照依赖关系的顺序执行。如果节点 x 依赖于节点 y,那么在请求执行 y 时,x 会在 y 之前执行。只有当节点本身或依赖于它的其他节点被调用执行时,节点才会被执行。这种执行哲学被称为懒加载。顾名思义,节点对象只有在实际需要时才会实例化和初始化。

通常,需要控制计算图中节点的执行顺序。这可以通过 tf.Graph.control_dependencies() 函数来完成。例如,如果图中有节点 lmno,并且我们希望在执行 lm 之前执行 no,那么我们可以使用以下代码:

with graph_variable.control_dependencies([n,o]):
  # other statements here

这确保了在前面的 with 块中,任何节点都会在节点 no 执行之后执行。

跨计算设备执行图 – CPU 和 GPGPU

图可以被划分为几个部分,每个部分可以在不同的设备上进行放置和执行,例如 CPU 或 GPU。所有可用于图执行的设备可以通过以下命令列出:

from tensorflow.python.client import device_lib
print(device_lib.list_local_devices())

输出如下所示(由于依赖于您系统中可用的计算设备,您的机器输出将有所不同):

[name: "/device:CPU:0"
device_type: "CPU"
memory_limit: 268435456
locality {
}
incarnation: 12900903776306102093
, name: "/device:GPU:0"
device_type: "GPU"
memory_limit: 611319808
locality {
  bus_id: 1
}
incarnation: 2202031001192109390
physical_device_desc: "device: 0, name: Quadro P5000, pci bus id: 0000:01:00.0, compute capability: 6.1"
]

TensorFlow 中的设备通过字符串 /device:<device_type>:<device_idx> 来标识。在最后的输出中,CPUGPU 表示设备类型,0 表示设备索引。

关于最后一个输出需要注意的一点是,它只显示了一个 CPU,而我们的计算机有 8 个 CPU。原因在于 TensorFlow 会隐式地将代码分配到 CPU 单元,因此默认情况下,CPU:0表示所有可用的 CPU。当 TensorFlow 开始执行图时,它会在每个图的独立路径中运行在不同线程上,每个线程运行在不同的 CPU 上。我们可以通过更改inter_op_parallelism_threads来限制用于此目的的线程数。类似地,如果在独立路径中某个操作能够在多个线程上运行,TensorFlow 会在多个线程上启动该操作。这个线程池中的线程数可以通过设置intra_op_parallelism_threads来更改。

将图节点放置在特定的计算设备上

通过定义一个配置对象来启用变量放置的日志记录,将log_device_placement属性设置为true,然后将这个config对象传递给会话,如下所示:

tf.reset_default_graph()

# Define model parameters
w = tf.Variable([.3], tf.float32)
b = tf.Variable([-.3], tf.float32)
# Define model input and output
x = tf.placeholder(tf.float32)
y = w * x + b

config = tf.ConfigProto()
config.log_device_placement=True

with tf.Session(config=config) as tfs:
   # initialize and print the variable y
   tfs.run(global_variables_initializer())
   print('output',tfs.run(y,{x:[1,2,3,4]}))

Jupyter Notebook 控制台窗口的输出如下所示:

b: (VariableV2): /job:localhost/replica:0/task:0/device:GPU:0
b/read: (Identity): /job:localhost/replica:0/task:0/device:GPU:0
b/Assign: (Assign): /job:localhost/replica:0/task:0/device:GPU:0
w: (VariableV2): /job:localhost/replica:0/task:0/device:GPU:0
w/read: (Identity): /job:localhost/replica:0/task:0/device:GPU:0
mul: (Mul): /job:localhost/replica:0/task:0/device:GPU:0
add: (Add): /job:localhost/replica:0/task:0/device:GPU:0
w/Assign: (Assign): /job:localhost/replica:0/task:0/device:GPU:0
init: (NoOp): /job:localhost/replica:0/task:0/device:GPU:0
x: (Placeholder): /job:localhost/replica:0/task:0/device:GPU:0
b/initial_value: (Const): /job:localhost/replica:0/task:0/device:GPU:0
Const_1: (Const): /job:localhost/replica:0/task:0/device:GPU:0
w/initial_value: (Const): /job:localhost/replica:0/task:0/device:GPU:0
Const: (Const): /job:localhost/replica:0/task:0/device:GPU:0

因此,默认情况下,TensorFlow 会在一个设备上创建变量和操作节点,以便获得最高的性能。这些变量和操作可以通过使用tf.device()函数将其放置在特定设备上。我们来将图放置在 CPU 上:

tf.reset_default_graph()

with tf.device('/device:CPU:0'):
    # Define model parameters
    w = tf.get_variable(name='w',initializer=[.3], dtype=tf.float32)
    b = tf.get_variable(name='b',initializer=[-.3], dtype=tf.float32)
    # Define model input and output
    x = tf.placeholder(name='x',dtype=tf.float32)
    y = w * x + b

config = tf.ConfigProto()
config.log_device_placement=True

with tf.Session(config=config) as tfs:
   # initialize and print the variable y
   tfs.run(tf.global_variables_initializer())
   print('output',tfs.run(y,{x:[1,2,3,4]}))

在 Jupyter 控制台中,我们可以看到变量已被放置在 CPU 上,并且执行也发生在 CPU 上:

b: (VariableV2): /job:localhost/replica:0/task:0/device:CPU:0
b/read: (Identity): /job:localhost/replica:0/task:0/device:CPU:0
b/Assign: (Assign): /job:localhost/replica:0/task:0/device:CPU:0
w: (VariableV2): /job:localhost/replica:0/task:0/device:CPU:0
w/read: (Identity): /job:localhost/replica:0/task:0/device:CPU:0
mul: (Mul): /job:localhost/replica:0/task:0/device:CPU:0
add: (Add): /job:localhost/replica:0/task:0/device:CPU:0
w/Assign: (Assign): /job:localhost/replica:0/task:0/device:CPU:0
init: (NoOp): /job:localhost/replica:0/task:0/device:CPU:0
x: (Placeholder): /job:localhost/replica:0/task:0/device:CPU:0
b/initial_value: (Const): /job:localhost/replica:0/task:0/device:CPU:0
Const_1: (Const): /job:localhost/replica:0/task:0/device:CPU:0
w/initial_value: (Const): /job:localhost/replica:0/task:0/device:CPU:0
Const: (Const): /job:localhost/replica:0/task:0/device:CPU:0

简单放置

TensorFlow 遵循以下规则来将变量放置在设备上:

If the graph was previously run, 
    then the node is left on the device where it was placed earlier
Else If the tf.device() block is used,
    then the node is placed on the specified device
Else If the GPU is present
    then the node is placed on the first available GPU
Else If the GPU is not present
    then the node is placed on the CPU

动态放置

tf.device()函数可以通过提供函数名称来替代设备字符串。如果提供了函数名称,则该函数必须返回设备字符串。通过自定义函数提供设备字符串的方式,可以使用复杂的算法来将变量放置在不同的设备上。例如,TensorFlow 提供了一个轮询设备设置函数tf.train.replica_device_setter()

软放置

如果一个 TensorFlow 操作被放置在 GPU 上,那么执行引擎必须具有该操作的 GPU 实现,这被称为内核。如果内核不存在,那么放置将导致运行时错误。此外,如果请求的 GPU 设备不存在,则会引发运行时错误。处理此类错误的最佳方法是允许操作在 GPU 设备请求失败时放置到 CPU 上。这可以通过设置以下config值来实现:

config.allow_soft_placement = True

GPU 内存处理

在 TensorFlow 会话开始时,默认情况下,会话会占用所有的 GPU 内存,即使操作和变量仅放置在多 GPU 系统中的一个 GPU 上。如果另一个会话同时开始执行,它将遇到内存不足错误。这个问题可以通过多种方式解决:

  • 对于多 GPU 系统,设置环境变量CUDA_VISIBLE_DEVICES=<device idx 列表>
os.environ['CUDA_VISIBLE_DEVICES']='0'

在此设置之后执行的代码将能够获取所有可见 GPU 的内存。

  • 要让会话只抓取 GPU 的一部分内存,请使用配置选项per_process_gpu_memory_fraction来分配内存的百分比:
config.gpu_options.per_process_gpu_memory_fraction = 0.5

这将分配 50%的内存到所有 GPU 设备上。

  • 通过结合前面两种策略,您可以让进程只看到 GPU 的一定百分比和部分 GPU。

  • 限制 TensorFlow 进程仅抓取启动时所需的最小内存。随着进程的进一步执行,可以设置配置选项,允许内存逐步增长:

config.gpu_options.allow_growth = True

这个选项仅允许已分配的内存增长,因此内存永远不会被释放回去。

若想了解更多关于分布式计算技术的学习方法,请参考我们的书籍《Mastering TensorFlow》。

多图

我们可以创建自己的图,这些图与默认图分开,并在会话中执行它们。然而,创建和执行多个图并不推荐,因为有以下缺点:

  • 在同一程序中创建和使用多个图需要多个 TensorFlow 会话,每个会话都会消耗大量资源

  • 数据不能直接在图之间传递

因此,推荐的方法是在一个图中使用多个子图。如果我们希望使用自己的图而不是默认图,可以使用tf.graph()命令来实现。在下面的示例中,我们创建了自己的图g,并将其作为默认图执行:

g = tf.Graph()
output = 0

# Assume Linear Model y = w * x + b

with g.as_default():
 # Define model parameters
 w = tf.Variable([.3], tf.float32)
 b = tf.Variable([-.3], tf.float32)
 # Define model input and output
 x = tf.placeholder(tf.float32)
 y = w * x + b

with tf.Session(graph=g) as tfs:
 # initialize and print the variable y
 tf.global_variables_initializer().run()
 output = tfs.run(y,{x:[1,2,3,4]})

print('output : ',output)

现在,让我们将所学的知识付诸实践,使用 TensorFlow 实现手写数字图像的分类。

机器学习、分类和逻辑回归

现在让我们学习机器学习、分类以及逻辑回归。

机器学习

机器学习指的是通过算法使计算机从数据中学习。计算机学习到的模型用于进行预测和预报。机器学习已经在多个领域取得了成功应用,例如自然语言处理、自动驾驶、图像与语音识别、聊天机器人以及计算机视觉。

机器学习算法大致可以分为三类:

  • 监督学习:在监督学习中,机器通过由特征和标签组成的训练数据集来学习模型。监督学习问题通常有两种类型:回归分类。回归是指基于模型预测未来的数值,而分类是指预测输入值的类别。

  • 无监督学习:在无监督学习中,机器从仅包含特征的训练数据集中学习模型。最常见的无监督学习类型之一称为聚类。聚类是指将输入数据分成多个组,从而产生聚类或分段。

  • 强化学习:在强化学习中,代理从初始模型开始,并根据来自环境的反馈持续学习模型。强化学习代理通过应用监督或无监督学习技术来学习或更新模型,作为强化学习算法的一部分。

这些机器学习问题在某种形式上被抽象为以下方程:

在这里,y表示目标x表示特征。如果x是特征的集合,则也称为特征向量,并用X表示。模型是函数f,它将特征映射到目标。一旦计算机学习了f,它就可以使用新的x值来预测y的值。

在机器学习线性模型的上下文中,可以将前述简单方程重写如下:

在这里,w被称为权重,b被称为偏置。因此,机器学习问题现在可以陈述为从当前的X值中找到wb的问题,以便可以用这个方程来预测y的值。

回归分析或回归建模指的是用于估计变量之间关系的方法和技术。用作回归模型输入的变量称为独立变量、预测变量或特征,而从回归模型得出的输出变量称为因变量或目标。回归模型定义如下:

这里,Y是目标变量,X是特征向量,β是参数向量(前述方程中的wb)。

分类

分类是机器学习中的经典问题之一。考虑的数据可能属于一类或另一类,例如,如果所提供的图像是数据,则它们可能是猫或狗的图片。因此,在这种情况下,类别就是猫和狗。分类意味着识别正在考虑的对象的标签或类别。分类属于监督机器学习的范畴。在分类问题中,提供了一个训练数据集,其中包含特征或输入及其相应的输出或标签。使用这个训练数据集,训练一个模型;换句话说,计算模型的参数。然后,训练好的模型被用于新数据,以找到其正确的标签。

分类问题可以分为两种类型:二分类多分类。二分类意味着数据要被分类成两个不同且离散的标签;例如,病人是否患癌症,或者图像是猫还是狗,等等。多分类意味着数据要在多个类别之间分类,例如,电子邮件分类问题会将电子邮件分为社交媒体邮件、工作相关邮件、个人邮件、家庭相关邮件、垃圾邮件、购物优惠邮件等。另一个例子是数字图像;每张图像的标签可以是 0 到 9 之间的任意数字,取决于该图像表示的是哪个数字。本章将会展示这两种分类问题的示例。

最流行的分类方法是逻辑回归。逻辑回归是一种概率性且线性的分类器。输入特征向量属于某一特定类别的概率可以通过以下数学方程来描述:

在上述方程中,以下内容适用:

  • Y 代表输出

  • i 代表类别之一

  • x 代表输入

  • w 代表权重

  • b 代表偏差

  • z 代表回归方程

  • ϕ 代表平滑函数(或者在我们案例中的模型)

ϕ(z) 函数表示在给定 wb 时,x 属于类别 i 的概率。因此,模型必须经过训练,以最大化此概率的值。

二分类的逻辑回归

对于二分类,模型函数 ϕ(z) 定义为 sigmoid 函数,其表达式如下:

Sigmoid 函数将 y 值转换为在 [0,1] 范围内。因此,y=ϕ(z) 的值可用于预测类别:如果 y > 0.5,则该对象属于 1,否则该对象属于 0。

模型训练是指寻找能最小化损失函数的参数,这些参数可以是平方误差的总和或均方误差的总和。对于逻辑回归,似然函数的最大化如下:

然而,由于最大化对数似然较为容易,我们使用对数似然 (l(w)) 作为成本函数。损失函数 (J(w)) 写作 -l(w),可以通过使用诸如梯度下降之类的优化算法进行最小化。

二分类逻辑回归的损失函数在数学上写作如下:

这里,ϕ(z) 是 sigmoid 函数。

多分类的逻辑回归

当涉及到多于两个类别时,逻辑回归被称为多项逻辑回归。在多项逻辑回归中,使用 softmax 函数代替 sigmoid 函数,该函数可以通过以下数学方式描述:

softmax 函数为每个类别生成概率,使得概率向量的和为 1。在推理时,具有最高 softmax 值的类别成为输出或预测的类别。正如我们之前讨论的,损失函数是负对数似然函数,-l(w),可以通过优化器(如梯度下降)进行最小化。

多项逻辑回归的损失函数正式写作如下:

这里,ϕ(z) 是 softmax 函数。

我们将在下一节中实现这个损失函数。在接下来的部分,我们将深入探讨如何使用 TensorFlow 进行多类分类的逻辑回归示例。

使用 TensorFlow 进行逻辑回归

关于多类分类的最流行示例之一是标记手写数字图像。在这个示例中,类别或标签是 {0,1,2,3,4,5,6,7,8,9}。我们将使用的数据集通常被称为 MNIST,可以通过以下链接获取:yann.lecun.com/exdb/mnist/。MNIST 数据集有 60,000 张用于训练的图像和 10,000 张用于测试的图像。数据集中的图像如下所示:

  1. 首先,我们必须导入 datasetslib,这是我们编写的一个库,用来帮助书中的示例(可以作为本书 GitHub 仓库的子模块获取):
DSLIB_HOME = '../datasetslib'
import sys
if not DSLIB_HOME in sys.path:
    sys.path.append(DSLIB_HOME)
%reload_ext autoreload
%autoreload 2
import datasetslib as dslib

from datasetslib.utils import imutil
from datasetslib.utils import nputil
from datasetslib.mnist import MNIST
  1. 设置我们主目录中 datasets 文件夹的路径,这是我们希望存储所有 datasets 的地方:
import os
datasets_root = os.path.join(os.path.expanduser('~'),'datasets')
  1. 使用我们的 datasetslib 获取 MNIST 数据,并打印数据形状以确保数据已正确加载:
mnist=MNIST()

x_train,y_train,x_test,y_test=mnist.load_data()

mnist.y_onehot = True
mnist.x_layout = imutil.LAYOUT_NP
x_test = mnist.load_images(x_test)
y_test = nputil.onehot(y_test)

print('Loaded x and y')
print('Train: x:{}, y:{}'.format(len(x_train),y_train.shape))
print('Test: x:{}, y:{}'.format(x_test.shape,y_test.shape))
  1. 定义训练模型的超参数:
learning_rate = 0.001
n_epochs = 5
mnist.batch_size = 100
  1. 为我们的简单模型定义占位符和参数:
# define input images
x = tf.placeholder(dtype=tf.float32, shape=[None, mnist.n_features])
# define output labels
y = tf.placeholder(dtype=tf.float32, shape=[None, mnist.n_classes])

# model parameters
w = tf.Variable(tf.zeros([mnist.n_features, mnist.n_classes]))
b = tf.Variable(tf.zeros([mnist.n_classes]))
  1. 使用 logitsy_hat 来定义模型:
logits = tf.add(tf.matmul(x, w), b)
y_hat = tf.nn.softmax(logits)
  1. 定义 loss 函数:
epsilon = tf.keras.backend.epsilon()
y_hat_clipped = tf.clip_by_value(y_hat, epsilon, 1 - epsilon)
y_hat_log = tf.log(y_hat_clipped)
cross_entropy = -tf.reduce_sum(y * y_hat_log, axis=1)
loss_f = tf.reduce_mean(cross_entropy)
  1. 定义 optimizer 函数:
optimizer = tf.train.GradientDescentOptimizer
optimizer_f = optimizer(learning_rate=learning_rate).minimize(loss_f)
  1. 定义一个函数来检查训练模型的准确率:
predictions_check = tf.equal(tf.argmax(y_hat, 1), tf.argmax(y, 1))
accuracy_f = tf.reduce_mean(tf.cast(predictions_check, tf.float32))
  1. 在 TensorFlow 会话中为每个 epoch 运行 training 循环:
n_batches = int(60000/mnist.batch_size)

with tf.Session() as tfs:
    tf.global_variables_initializer().run()
    for epoch in range(n_epochs):
        mnist.reset_index()
        for batch in range(n_batches):
            x_batch, y_batch = mnist.next_batch()
            feed_dict={x: x_batch, y: y_batch}
            batch_loss,_ = tfs.run([loss_f, optimizer_f],feed_dict=feed_dict )
            #print('Batch loss:{}'.format(batch_loss))

  1. 在之前创建的 TensorFlow 会话中,针对每个 epoch 使用测试数据运行评估函数:
feed_dict = {x: x_test, y: y_test}
accuracy_score = tfs.run(accuracy_f, feed_dict=feed_dict)
print('epoch {0:04d}  accuracy={1:.8f}'
      .format(epoch, accuracy_score))

我们得到以下输出:

epoch 0000 accuracy=0.73280001 epoch 0001 accuracy=0.72869998 epoch 0002 accuracy=0.74550003 epoch 0003 accuracy=0.75260001 epoch 0004 accuracy=0.74299997

就这样。我们刚刚使用 TensorFlow 训练了我们的第一个逻辑回归模型,用于对手写数字图像进行分类,并且得到了 74.3% 的准确率。

现在,让我们看看在 Keras 中编写相同模型是如何让这个过程变得更简单的。

使用 Keras 进行逻辑回归

Keras 是一个高级库,它作为 TensorFlow 的一部分提供。在这一节中,我们将用 Keras 重建我们之前用 TensorFlow 核心构建的相同模型:

  1. Keras 以不同的格式接收数据,因此我们必须首先使用 datasetslib 重新格式化数据:
x_train_im = mnist.load_images(x_train)

x_train_im, x_test_im = x_train_im / 255.0, x_test / 255.0

在前面的代码中,我们在训练和测试图像被缩放之前将训练图像加载到内存中,我们通过将其除以 255 来实现缩放。

  1. 然后,我们构建模型:
model = tf.keras.models.Sequential([
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(10, activation=tf.nn.softmax)
])
  1. 使用 sgd 优化器编译模型。将分类熵作为 loss 函数,准确率作为测试模型的指标:
model.compile(optimizer='sgd',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
  1. 使用训练集图像和标签训练模型 5 个周期:
model.fit(x_train_im, y_train, epochs=5)

Epoch 1/5
60000/60000 [==============================] - 3s 45us/step - loss: 0.7874 - acc: 0.8095
Epoch 2/5
60000/60000 [==============================] - 3s 42us/step - loss: 0.4585 - acc: 0.8792
Epoch 3/5
60000/60000 [==============================] - 2s 42us/step - loss: 0.4049 - acc: 0.8909
Epoch 4/5
60000/60000 [==============================] - 3s 42us/step - loss: 0.3780 - acc: 0.8965
Epoch 5/5
60000/60000 [==============================] - 3s 42us/step - loss: 0.3610 - acc: 0.9012
10000/10000 [==============================] - 0s 24us/step
  1. 使用测试数据评估模型:
model.evaluate(x_test_im, nputil.argmax(y_test))

我们得到以下评估分数作为输出:

[0.33530342621803283, 0.9097]

哇!使用 Keras,我们可以实现更高的准确性。我们达到了大约 90% 的准确率。这是因为 Keras 在内部为我们设置了许多最优值,使我们能够快速开始构建模型。

要了解更多关于 Keras 的信息并查看更多示例,请参考 Packt 出版社的书籍 精通 TensorFlow

总结

在本章中,我们简要介绍了 TensorFlow 库。我们介绍了 TensorFlow 数据模型元素,如常量、变量和占位符,以及如何使用它们构建 TensorFlow 计算图。我们学习了如何从 Python 对象创建张量。张量对象还可以作为特定值、序列或从各种 TensorFlow 库函数生成的随机值分布来生成。

我们介绍了 TensorFlow 编程模型,包括定义和执行计算图。这些计算图包含节点和边。节点代表操作,边代表在节点之间传输数据的张量。我们讲解了如何创建和执行图、执行顺序以及如何在多个计算设备上执行图,如 CPU 和 GPU。

我们还了解了机器学习,并实现了一个分类算法来识别手写数字数据集。我们实现的算法被称为多项式逻辑回归。我们使用了 TensorFlow 核心和 Keras 来实现逻辑回归算法。

从下一章开始,我们将查看许多使用 TensorFlow 和 Keras 实现的项目。

问题

通过练习以下问题来加深理解:

  1. 修改本章中给出的逻辑回归模型,以便使用不同的训练率,并观察其对训练的影响。

  2. 使用不同的优化器函数,观察不同函数对训练时间和准确度的影响。

进一步阅读

我们建议读者通过阅读以下材料来进一步学习:

  • 精通 TensorFlow 由 Armando Fandango 编写。

  • TensorFlow 教程请参见 www.tensorflow.org/tutorials/

  • TensorFlow 1.x 深度学习实用宝典 由 Antonio Gulli 和 Amita Kapoor 编写。

第二章:使用机器学习检测外星行星

在本章中,我们将学习如何使用基于决策树的集成方法来检测外星行星。

决策树是一类非参数的监督学习方法。在决策树算法中,数据通过使用一个简单规则被分为两个部分。这个规则被一遍遍应用,继续将数据划分,从而形成一棵决策树。

集成方法将多个学习算法的学习成果结合起来,以提高预测准确性并减少误差。这些集成方法根据它们使用的学习器种类以及如何在集成中结构化这些学习器而有所不同。

基于决策树的两种最流行的集成方法分别是梯度提升树和随机森林。

本章将涵盖以下主题:

  • 什么是决策树?

  • 为什么我们需要集成方法?

  • 基于决策树的集成方法

    • 随机森林

    • 梯度提升

  • 基于决策树的集成方法在 TensorFlow 中的应用

  • 构建一个用于外星行星检测的 TensorFlow 提升树模型

本章的代码可在 Jupyter Notebook 中找到,文件名为 ch-02_Detecting_Exoplanets_in_Outer_Space.ipynb,包含在代码包中。

什么是决策树?

决策树是一类非参数的监督学习方法。在决策树算法中,我们从完整的数据集开始,并根据一个简单规则将其分为两部分。这个划分会持续进行,直到满足指定的标准为止。做出划分的节点称为内部节点,最终的端点称为终端节点或叶节点。

作为示例,我们来看以下这棵树:

在这里,我们假设外星行星数据只有两个特征:flux.1flux.2。首先,我们根据条件 flux.1 > 400 做出决策,然后将数据分为两个部分。接着,我们基于 flux.2 特征再次划分数据,这个划分决定了该行星是否为外星行星。我们是如何决定 flux.1 > 400 这个条件的呢?我们并没有。这只是用来演示决策树。在训练阶段,模型学习到的就是这些划分数据的条件参数。

对于分类问题,决策树的叶节点展示结果作为数据的离散分类;对于回归问题,叶节点展示结果作为预测的数值。因此,决策树也被广泛称为分类与回归树CART)。

为什么我们需要集成方法?

决策树容易对训练数据过拟合,并且受到高方差的影响,因此,无法对新的未见数据做出良好的预测。然而,使用决策树集成可以缓解单一决策树模型的不足。在集成中,多个弱学习器联合起来形成一个强大的学习器。

在我们可以将决策树组合成集成的多种方式中,由于在预测建模中的表现,以下两种方法是最受欢迎的:

  • 梯度提升(也称为梯度树提升)

  • 随机决策树(也称为随机森林)

基于决策树的集成方法

在这一部分,我们简要探讨两种决策树集成方法:随机森林和梯度提升。

随机森林

随机森林是一种技术,其中你构建多棵树,然后使用这些树来学习分类和回归模型,但结果是通过聚合这些树的输出得出的,最终产生一个结果。

随机森林是由一组随机、无关且完全生长的决策树组成的集成模型。随机森林模型中使用的决策树是完全生长的,因此具有低偏差和高方差。这些树本质上是无关的,从而导致方差的最大减少。这里所说的无关,意味着随机森林中的每棵决策树都被分配到一个随机选择的特征子集,并且从该特征子集中随机选择一个数据子集。

描述随机森林的原始论文可以通过以下链接获取:www.stat.berkeley.edu/~breiman/randomforest2001.pdf

随机森林技术并不会减少偏差,因此与集成中的单棵决策树相比,具有稍高的偏差。

随机森林是由 Leo Breiman 发明的,并且已由 Leo Breiman 和 Adele Cutler 注册商标。更多信息请访问以下链接:www.stat.berkeley.edu/~breiman/RandomForests

直观地说,在随机森林模型中,大量的决策树在不同的数据样本上进行训练,这些样本可能会拟合或过拟合。通过对个别决策树的结果取平均,过拟合会被消除。

随机森林看起来与装袋(Bagging)类似,即自助聚合,但它们是不同的。在装袋中,每棵树的训练数据都是通过有放回的随机抽样来选择的,且树是基于所有特征进行训练的。在随机森林中,特征也是随机采样的,并且在每次候选分裂时,使用特征的子集来训练模型。

在回归问题中,随机森林模型通过对个别决策树的预测结果取平均来进行值的预测。在分类问题中,随机森林模型通过对个别决策树的结果进行多数投票来进行类的预测。

随机森林的有趣解释可以通过以下链接查看:machinelearning-blog.com/2018/02/06/the-random-forest-algorithm/

梯度提升

梯度提升树是浅层决策树(或弱学习器)的集成。浅层决策树可以小到只有两片叶子的树(也叫做决策桩)。提升方法主要帮助减少偏差,同时也能稍微减少方差。

Breiman 和 Friedman 的原始论文,提出了梯度提升的理念,可以通过以下链接查看:

直观地说,在梯度提升模型中,集成中的决策树经过多次迭代训练,如下图所示。每次迭代都会增加一棵新的决策树。每一棵额外的决策树都是为了改进之前迭代中训练的集成模型。这与随机森林模型不同,后者中的每棵决策树都是独立训练的,不受其他决策树的影响。

与随机森林模型相比,梯度提升模型的树木数量较少,但最终会有大量需要调优的超参数,以获得一个合格的梯度提升模型。

关于梯度提升的一个有趣解释,可以通过以下链接找到:blog.kaggle.com/2017/01/23/a-kaggle-master-explains-gradient-boosting/

基于决策树的集成在 TensorFlow 中的实现

在本章中,我们将使用由 Google TensorFlow 团队提供的预先构建的梯度提升树和随机森林实现作为估算器。接下来的章节中我们将学习它们的实现细节。

TensorForest 估算器

TensorForest 是一个高度可扩展的随机森林实现,它通过结合各种在线 HoeffdingTree 算法与极度随机化方法构建而成。

Google 在以下论文中发布了 TensorForest 实现的详细信息:TensorForest:在 TensorFlow 上可扩展的随机森林,由 Thomas Colthurst、D. Sculley、Gibert Hendry、Zack Nado 撰写,并在 2016 年神经信息处理系统会议(NIPS)的机器学习系统研讨会上发布。该论文可以通过以下链接访问:docs.google.com/viewer?a=v&pid=sites&srcid=ZGVmYXVsdGRvbWFpbnxtbHN5c25pcHMyMDE2fGd4OjFlNTRiOWU2OGM2YzA4MjE

TensorForest 估算器用于实现以下算法:

Initialize the variables and sets
    Tree = [root]
    Fertile = {root}
    Stats(root) = 0
    Splits[root] = []

Divide training data into batches.
For each batch of training data:
    Compute leaf assignment  for each feature vector
    Update the leaf stats in Stats()
    For each  in Fertile set:
        if |Splits()| < max_splits
            then add the split on a randomly selected feature to Splits()
        else if  is fertile and |Splits()| = max_splits
            then update the split stats for 
    Calculate the fertile leaves that are finished. 
    For every non-stale finished leaf:
        turn the leaf into an internal node with its best scoring split 
        remove the leaf from Fertile
        add the leaf's two children to Tree as leaves
    If |Fertile| < max_fertile
        Then add the max_fertile − |Fertile| leaves with 
        the highest weighted leaf scores to Fertile and 
        initialize their Splits and split statistics. 
Until |Tree| = max_nodes or |Tree| stays the same for max_batches_to_grow batches 

有关该算法实现的更多细节,请参考《TensorForest》论文。

TensorFlow 增强树估算器

TensorFlow 增强树TFBT)是建立在通用梯度提升树之上的一种改进的可扩展集成模型。

Google 在以下论文中发布了 TensorFlow 增强树实现的详细信息:一种可扩展的基于 TensorFlow 的梯度提升框架,由 Natalia Ponomareva、Soroush Radpour、Gilbert Hendry、Salem Haykal、Thomas Colthurst、Petr Mitrichev、Alexander Grushetsky 撰写,并在 2017 年欧洲机器学习与数据库知识发现原理与实践会议(ECML PKDD)上发布。该论文可以通过以下链接访问:ecmlpkdd2017.ijs.si/papers/paperID705.pdf

梯度提升算法由 sklearnMLLibXGBoost 等各种库实现。TensorFlow 的实现与这些实现有所不同,具体内容可参考以下从 TFBT 研究论文中提取的表格:

Google 的 TFBT 研究论文

TFBT 模型可以通过在 TensorFlow 中编写自定义损失函数进行扩展。TensorFlow 会自动提供这些自定义损失函数的求导。

检测外太空中的系外行星

对于本章中解释的项目,我们使用来自 Kaggle 的 Kepler 标记时间序列数据www.kaggle.com/keplersmachines/kepler-labelled-time-series-data/home。该数据集主要来源于 NASA 的 Kepler 太空望远镜对任务 3 期的观测数据。

在数据集中,第 1 列的值为标签,第 2 至 3198 列的值为随时间变化的通量值。训练集包含 5087 个数据点,其中 37 个为已确认的系外行星,5050 个为非系外行星的恒星。测试集包含 570 个数据点,其中 5 个为已确认的系外行星,565 个为非系外行星的恒星。

我们将执行以下步骤来下载数据,并对数据进行预处理,以创建训练集和测试集:

  1. 使用 Kaggle API 下载数据集。以下代码将用于实现这一目的:
armando@librenix:~/datasets/kaggle-kepler$ kaggle datasets download -d keplersmachines/kepler-labelled-time-series-data

Downloading kepler-labelled-time-series-data.zip to /mnt/disk1tb/datasets/kaggle-kepler
100%|██████████████████████████████████████| 57.4M/57.4M [00:03<00:00, 18.3MB/s]

文件夹中包含以下两个文件:

exoTest.csv
exoTrain.csv
  1. 将文件夹datasets链接到我们的主文件夹,这样我们就可以通过~/datasets/kaggle-kepler路径访问它,然后在 Notebook 中定义文件夹路径并列出文件夹内容,以确认是否可以通过 Notebook 访问数据文件:
dsroot = os.path.join(os.path.expanduser('~'),'datasets','kaggle-kepler')
os.listdir(dsroot)

我们获得了如下输出:

['exoTest.csv', 'kepler-labelled-time-series-data.zip', 'exoTrain.csv']

ZIP 文件只是下载过程中的残留文件,因为 Kaggle API 首先下载 ZIP 文件,然后在同一文件夹中解压内容。

  1. 然后我们将读取两个.csv数据文件,分别存储在名为traintestpandas数据框中:
import pandas as pd
train = pd.read_csv(os.path.join(dsroot,'exoTrain.csv'))
test = pd.read_csv(os.path.join(dsroot,'exoTest.csv'))
print('Training data\n',train.head())
print('Test data\n',test.head())

trainingtest data的前五行大致如下:

Training data
    LABEL   FLUX.1   FLUX.2   FLUX.3  \
0      2    93.85    83.81    20.10     
1      2   -38.88   -33.83   -58.54   
2      2   532.64   535.92   513.73    
3      2   326.52   347.39   302.35    
4      2 -1107.21 -1112.59 -1118.95 
     FLUX.4   FLUX.5   FLUX.6  FLUX.7  \
0    -26.98   -39.56  -124.71 -135.18   
1    -40.09   -79.31   -72.81  -86.55   
2    496.92   456.45   466.00  464.50   
3    298.13   317.74   312.70  322.33   
4  -1095.10 -1057.55 -1034.48 -998.34   

    FLUX.8  FLUX.9    ...      FLUX.3188  \
0   -96.27  -79.89    ...         -78.07      
1   -85.33  -83.97    ...          -3.28   
2   486.39  436.56    ...         -71.69  
3   311.31  312.42    ...           5.71      
4 -1022.71 -989.57    ...        -594.37    

    FLUX.3189  FLUX.3190  FLUX.3191  \
0     -102.15    -102.15      25.13   
1      -32.21     -32.21     -24.89   
2       13.31      13.31     -29.89   
3       -3.73      -3.73      30.05   
4     -401.66    -401.66    -357.24  

   FLUX.3192  FLUX.3193  FLUX.3194  
0      48.57      92.54      39.32  
1      -4.86       0.76     -11.70     
2     -20.88       5.06     -11.80    
3      20.03     -12.67      -8.77      
4    -443.76    -438.54    -399.71   

   FLUX.3195  FLUX.3196  FLUX.3197  
0      61.42       5.08     -39.54  
1       6.46      16.00      19.93  
2     -28.91     -70.02     -96.67  
3     -17.31     -17.35      13.98  
4    -384.65    -411.79    -510.54  

[5 rows x 3198 columns]

Test data

    LABEL   FLUX.1   FLUX.2   FLUX.3   \
0      2   119.88   100.21    86.46      
1      2  5736.59  5699.98  5717.16    
2      2   844.48   817.49   770.07    
3      2  -826.00  -827.31  -846.12    
4      2   -39.57   -15.88    -9.16      

       FLUX.4   FLUX.5   FLUX.6   FLUX.7  \
0       48.68    46.12    39.39    18.57   
1     5692.73  5663.83  5631.16  5626.39   
2      675.01   605.52   499.45   440.77   
3     -836.03  -745.50  -784.69  -791.22   
4       -6.37   -16.13   -24.05    -0.90   

    FLUX.8   FLUX.9    ...      FLUX.3188  \
0     6.98     6.63    ...          14.52       
1  5569.47  5550.44    ...        -581.91    
2   362.95   207.27    ...          17.82     
3  -746.50  -709.53    ...         122.34       
4   -45.20    -5.04    ...         -37.87     
    FLUX.3189  FLUX.3190  FLUX.3191  \
0       19.29      14.44      -1.62   
1     -984.09   -1230.89   -1600.45   
2      -51.66     -48.29     -59.99   
3       93.03      93.03      68.81   
4      -61.85     -27.15     -21.18     

   FLUX.3192  FLUX.3193  FLUX.3194  \  
0      13.33      45.50      31.93    
1   -1824.53   -2061.17   -2265.98     
2     -82.10    -174.54     -95.23      
3       9.81      20.75      20.25    
4     -33.76     -85.34     -81.46    

   FLUX.3195  FLUX.3196  FLUX.3197  
0      35.78     269.43      57.72  
1   -2366.19   -2294.86   -2034.72  
2    -162.68     -36.79      30.63  
3    -120.81    -257.56    -215.41  
4     -61.98     -69.34     -17.84  

[5 rows x 3198 columns]

训练和测试数据集的标签位于第一列,接下来的列中有 3197 个特征。现在,我们使用以下代码将训练和测试数据拆分为标签和特征:

x_train = train.drop('LABEL', axis=1)
y_train = train.LABEL-1 #subtract one because of TGBT
x_test = test.drop('LABEL', axis=1)
y_test = test.LABEL-1

在上述代码中,我们从标签中减去1,因为 TFBT 估算器假设标签从数字零开始,而数据集中的特征是数字 1 和 2。

现在我们已经有了训练和测试数据的标签和特征向量,让我们来构建增强树模型。

构建一个用于外星行星探测的 TFBT 模型

在本节中,我们将构建用于检测外星行星的梯度增强树模型,使用 Kepler 数据集。让我们按照以下步骤在 Jupyter Notebook 中构建并训练外星行星探测模型:

  1. 我们将使用以下代码将所有特征的名称保存到一个向量中:
numeric_column_headers = x_train.columns.values.tolist()
  1. 然后,我们将特征列根据均值分成两个桶,因为 TFBT 估算器只接受桶化后的特征,代码如下:
bc_fn = tf.feature_column.bucketized_column
nc_fn = tf.feature_column.numeric_column
bucketized_features = [bc_fn(source_column=nc_fn(key=column),
                             boundaries=[x_train[column].mean()])
                       for column in numeric_column_headers]
  1. 由于我们只有数值型桶化特征,没有其他类型的特征,我们将它们存储在all_features变量中,代码如下:
all_features = bucketized_features
  1. 然后我们将定义批次大小,并创建一个函数,用于提供从训练数据中创建的标签和特征向量的输入。为创建这个函数,我们使用 TensorFlow 提供的便利函数tf.estimator.inputs.pandas_input_fn(),代码如下:
batch_size = 32
pi_fn = tf.estimator.inputs.pandas_input_fn
train_input_fn = pi_fn(x = x_train,
                       y = y_train,
                       batch_size = batch_size,
                       shuffle = True,
                       num_epochs = None)
  1. 类似地,我们将创建另一个数据输入函数,用于从测试特征和标签向量评估模型,并命名为eval_input_fn,使用以下代码:
eval_input_fn = pi_fn(x = x_test,
                      y = y_test,
                      batch_size = batch_size,
                      shuffle = False,
                      num_epochs = 1)
  1. 我们将定义要创建的树的数量为100,并将用于训练的步骤数定义为100。我们还将BoostedTreeClassifier定义为estimator,使用以下代码:
n_trees = 100
n_steps = 100

m_fn = tf.estimator.BoostedTreesClassifier
model = m_fn(feature_columns=all_features,
             n_trees = n_trees,
             n_batches_per_layer = batch_size,
             model_dir='./tfbtmodel')

由于我们正在进行分类,因此使用BoostedTreesClassifier,对于需要预测值的回归问题,TensorFlow 还有一个名为BoostedTreesRegressorestimator

提供给estimator函数的参数之一是model_dir,它定义了训练后的模型存储位置。估计器的构建方式使得在后续调用时,它们会在该文件夹中查找模型,以便用于推理和预测。我们将文件夹命名为tfbtmodel来保存模型。

我们使用了最少数量的模型来定义BoostedTreesClassifier。请查阅 TensorFlow API 文档中该估计器的定义,了解可以提供的其他参数,以进一步自定义该估计器。

Jupyter Notebook 中的以下输出描述了分类器估计器及其各种设置:

INFO:tensorflow:Using default config.
INFO:tensorflow:Using config: {'_model_dir': './tfbtmodel', '_tf_random_seed': None, '_save_summary_steps': 100, '_save_checkpoints_steps': None, '_save_checkpoints_secs': 600, '_session_config': None, '_keep_checkpoint_max': 5, '_keep_checkpoint_every_n_hours': 10000, '_log_step_count_steps': 100, '_train_distribute': None, '_device_fn': None, '_service': None, '_cluster_spec': <tensorflow.python.training.server_lib.ClusterSpec object at 0x7fdd48c93b38>, '_task_type': 'worker', '_task_id': 0, '_global_id_in_cluster': 0, '_master': '', '_evaluation_master': '', '_is_chief': True, '_num_ps_replicas': 0, '_num_worker_replicas': 1}
  1. 接下来,我们将使用train_input_fn函数训练模型,使用 100 步的输入数据进行外行星数据的训练,代码如下:
model.train(input_fn=train_input_fn, steps=n_steps)

Jupyter Notebook 显示以下输出,表示训练正在进行:

INFO:tensorflow:Calling model_fn.
INFO:tensorflow:Done calling model_fn.
INFO:tensorflow:Create CheckpointSaverHook.
WARNING:tensorflow:Issue encountered when serializing resources.
Type is unsupported, or the types of the items don't match field type in CollectionDef. Note this is a warning and probably safe to ignore.
'_Resource' object has no attribute 'name'
INFO:tensorflow:Graph was finalized.
INFO:tensorflow:Restoring parameters from ./tfbtmodel/model.ckpt-19201
INFO:tensorflow:Running local_init_op.
INFO:tensorflow:Done running local_init_op.
WARNING:tensorflow:Issue encountered when serializing resources.
Type is unsupported, or the types of the items don't match field type in CollectionDef. Note this is a warning and probably safe to ignore.
'_Resource' object has no attribute 'name'
INFO:tensorflow:Saving checkpoints for 19201 into ./tfbtmodel/model.ckpt.
WARNING:tensorflow:Issue encountered when serializing resources.
Type is unsupported, or the types of the items don't match field type in CollectionDef. Note this is a warning and probably safe to ignore.
'_Resource' object has no attribute 'name'
INFO:tensorflow:loss = 1.0475121e-05, step = 19201
INFO:tensorflow:Saving checkpoints for 19202 into ./tfbtmodel/model.ckpt.
WARNING:tensorflow:Issue encountered when serializing resources.
Type is unsupported, or the types of the items don't match field type in CollectionDef. Note this is a warning and probably safe to ignore.
'_Resource' object has no attribute 'name'
INFO:tensorflow:Loss for final step: 1.0475121e-05.
  1. 使用eval_input_fn,它提供来自test数据集的批次数据,以通过以下代码评估模型:
results = model.evaluate(input_fn=eval_input_fn)

Jupyter Notebook 显示以下输出,表示评估的进展:

INFO:tensorflow:Calling model_fn.
WARNING:tensorflow:Trapezoidal rule is known to produce incorrect PR-AUCs; please switch to "careful_interpolation" instead.
WARNING:tensorflow:Trapezoidal rule is known to produce incorrect PR-AUCs; please switch to "careful_interpolation" instead.
INFO:tensorflow:Done calling model_fn.
INFO:tensorflow:Starting evaluation at 2018-09-07-04:23:31
INFO:tensorflow:Graph was finalized.
INFO:tensorflow:Restoring parameters from ./tfbtmodel/model.ckpt-19203
INFO:tensorflow:Running local_init_op.
INFO:tensorflow:Done running local_init_op.
INFO:tensorflow:Finished evaluation at 2018-09-07-04:23:50
INFO:tensorflow:Saving dict for global step 19203: accuracy = 0.99122804, accuracy_baseline = 0.99122804, auc = 0.49911517, auc_precision_recall = 0.004386465, average_loss = 0.09851996, global_step = 19203, label/mean = 0.00877193, loss = 0.09749381, precision = 0.0, prediction/mean = 4.402521e-05, recall = 0.0
WARNING:tensorflow:Issue encountered when serializing resources.
Type is unsupported, or the types of the items don't match field type in CollectionDef. Note this is a warning and probably safe to ignore.
'_Resource' object has no attribute 'name'
INFO:tensorflow:Saving 'checkpoint_path' summary for global step 19203: ./tfbtmodel/model.ckpt-19203

注意,在评估过程中,估计器会加载保存在检查点文件中的参数:

INFO:tensorflow:Restoring parameters from ./tfbtmodel/model.ckpt-19203
  1. 评估结果存储在results集合中。我们可以使用以下代码中的for循环打印results集合中的每一项:
for key,value in sorted(results.items()):
    print('{}: {}'.format(key, value))

Notebook 显示以下结果:

accuracy: 0.9912280440330505
accuracy_baseline: 0.9912280440330505
auc: 0.4991151690483093
auc_precision_recall: 0.004386465065181255
average_loss: 0.0985199585556984
global_step: 19203
label/mean: 0.008771929889917374
loss: 0.09749381244182587
precision: 0.0
prediction/mean: 4.4025211536791176e-05
recall: 0.0

观察到,我们在第一个模型中就达到了近 99%的准确率。这是因为估计器已预先写入了多个优化选项,我们无需自己设置超参数的各个值。对于某些数据集,估计器中的默认超参数值可以直接使用,但对于其他数据集,您需要调整估计器的各种输入值。

总结

本章中,我们了解了什么是决策树,以及从决策树创建集成方法的两大类。我们关注的集成方法是随机森林和梯度提升树。

我们还了解了 Kaggle 竞赛中的开普勒数据集。我们使用开普勒数据集构建了一个外行星检测模型,使用 TensorFlow 预构建的梯度提升树估计器BoostedTreesClassifierBoostedTreesClassifier估计器是 TensorFlow 团队最近发布的机器学习工具包的一部分。目前,TensorFlow 团队正在开发基于支持向量机SVM)和极端随机森林的预构建估计器,并作为tf.estimators API 的一部分发布。

在下一章中,我们将学习如何使用浏览器中的TensorFlow.js API 进行情感分析。

问题

  • 梯度提升与随机森林有何不同?

  • 如何提高随机森林的性能?

  • 如何提高梯度提升树的性能?

  • 如何确保梯度提升树和随机森林不会过拟合?

  • 修改本章中的模型,使用不同的参数,如树的数量、批处理大小、训练周期数和步数,并观察它们对训练时间和不同准确度水平的影响。

进一步阅读

第三章:在浏览器中使用 TensorFlow.js 进行情感分析

情感分析是机器学习中的一个热门问题。人们不断尝试理解产品或电影评论的情感。目前,对于情感分析,我们从客户端/浏览器提取文本,将其传递给运行机器学习模型的服务器来预测文本的情感,服务器然后将结果返回给客户端。

如果我们不关心系统的延迟,这样做是完全可以接受的。然而,很多应用场景(如股票交易、客户支持对话)中,预测文本的情感低延迟可能会非常有帮助。减少延迟的一个明显瓶颈就是服务器调用。

如果情感分析能够在浏览器/客户端本身完成,我们就可以摆脱服务器调用,并且可以实时预测情感。谷歌最近发布了 TensorFlow.js,它使我们能够在浏览器/客户端上进行模型训练和推理。

此外,在客户端训练模型也打开了一个全新的机会。以下是这样做的所有优势的简要总结:

  • 隐私:由于数据永远不会离开客户端,我们能够提供机器学习的神奇体验而不妥协于数据隐私

  • 无烦恼的机器学习:由于代码在浏览器中运行,用户不需要安装任何库或依赖项

  • 低延迟:由于不需要将数据传输到服务器进行训练/预测,我们可以为低延迟应用部署机器学习模型

  • 设备无关性:一个网页可以在任何设备(笔记本、手机等)上打开,因此 TensorFlow.js 可以利用任何硬件(GPU)或设备传感器,如手机中的加速度计,来训练机器学习模型

让我们通过以下主题来学习如何使用 TensorFlow.js 在浏览器中整合情感分析:

  • 理解 TensorFlow.js

  • 理解 Adam 优化算法

  • 理解类别交叉熵损失

  • 理解词嵌入

  • 在 Python 中设置情感分析问题并构建模型

  • 使用 TensorFlow.js 在浏览器中部署模型

在本章中查找代码,并且安装说明也在此项目的 README 文件中。

理解 TensorFlow.js

TensorFlow 最近开源了 TensorFlow.js。这是一个开源库,帮助我们完全在浏览器中使用 JavaScript 定义和训练深度学习模型,同时提供高级分层 API。我们可以利用它在客户端完全训练深度学习模型,不需要服务器 GPU 来训练这些模型。

下图展示了 TensorFlow.js 的 API 概览:

这由 WebGL 提供支持。它还提供了一个高级分层 API,并且支持 Eager 执行。你可以使用 TensorFlow.js 实现以下三项功能:

  • 在浏览器中加载现有的 TensorFlow/Keras 模型以进行预测

  • 使用客户数据重新训练现有模型

  • 浏览器上定义和训练深度学习模型

理解 Adam 优化

在我们研究 Adam 优化之前,让我们先试着理解梯度下降的概念。

梯度下降是一种迭代优化算法,用于寻找函数的最小值。类比的例子可以是:假设我们被困在山的中部某处,我们想以最快的方式到达地面。作为第一步,我们会观察周围所有方向山的坡度,并决定沿最陡的坡度方向向下前进。

每走一步后,我们重新评估我们的方向选择。此外,我们步行的距离也取决于下坡的陡峭程度。如果坡度很陡,我们会迈更大的步伐,因为这有助于我们更快地到达地面。这样,在经过几步或大量步骤后,我们可以安全地到达地面。类似地,在机器学习中,我们希望通过更新算法的权重来最小化某些错误/成本函数。为了使用梯度找到成本函数的最小值,我们根据最陡下降的方向中的梯度更新算法的权重。在神经网络文献中,这个比例常数也被称为学习率。

然而,在大规模机器学习中,进行梯度下降优化是相当昂贵的,因为我们在整个训练数据集通过后只能进行一步。因此,如果我们要花费几千步来收敛到成本函数的最小值,那么收敛时间将会非常长。

解决这个问题的一个解决方案是随机梯度下降(SGD),这是一种在每个训练示例后更新算法权重的方法,而不是等待整个训练数据集通过算法进行更新。我们使用术语“随机”来表示梯度的近似性,因为它仅在每个训练示例后计算。然而,文献中表明,经过许多迭代,SGD 几乎肯定会收敛到函数的真实局部/全局最小值。一般来说,在深度学习算法中,我们观察到我们倾向于使用小批量 SGD,在每个小批量后更新权重,而不是在每个训练示例后更新。

Adam 优化是 SGD 的一个变体,我们在每个参数(权重)上维护学习率,并根据该参数之前梯度的均值和方差进行更新。已经证明 Adam 对许多深度学习问题非常高效和快速。有关 Adam 优化的更多细节,请参阅原始论文(arxiv.org/abs/1412.6980)。

理解分类交叉熵损失

交叉熵损失,或称对数损失,衡量分类模型的性能,该模型的输出是介于 0 和 1 之间的概率。当预测概率与实际值偏离时,交叉熵增加。因此,当实际标签值为 1 时,如果预测概率为 0.05,交叉熵损失就会增加。

数学上,对于二分类设置,交叉熵定义为以下方程:

这里, 是一个二进制指示符(0 或 1),表示样本 的类别,而 表示该样本的预测概率,范围是 0 到 1。

另一种情况是,如果类别数超过两个,我们定义一个新的术语,称为类别交叉熵。它是通过对每个类别标签在每个观察值上的单独损失求和来计算的。从数学上讲,它由以下方程给出:

这里, 表示类别的数量, 是一个二进制指示符(0 或 1),表示 是否是该观察的正确类别,,以及 表示该观察属于类别 的概率。

在本章中,由于我们对评论进行二分类,因此我们只会使用二进制交叉熵作为分类损失。

理解词嵌入

词嵌入是指自然语言处理NLP)中用于生成单词、句子或文档的实数值向量表示的特征学习技术类别。

现在许多机器学习任务都涉及文本。例如,谷歌的语言翻译或 Gmail 中的垃圾邮件检测都将文本作为输入,使用模型执行翻译和垃圾邮件检测任务。然而,现代计算机只能接受实值数字作为输入,无法理解字符串或文本,除非我们将其编码为数字或向量。

举个例子,假设我们有一句话 "I like Football",我们希望获取所有单词的表示。生成三词 "I"、"like" 和 "Football" 的嵌入的粗暴方法是通过单词的独热表示来实现的。在这种情况下,嵌入结果如下:

  • "I" = [1,0,0]

  • "like" = [0,1,0]

  • "Football" = [0,0,1]

这个方法的思路是创建一个向量,其维度等于句子中唯一单词的数量,并且在单词出现的地方赋值为 1,在其他地方为 0。这个方法存在两个问题:

  • 向量维度的数量随着语料库中单词数量的增加而增加。假设我们有 100,000 个独特的单词在文档中。因此,我们用一个维度为 100,000 的向量来表示每个单词。这增加了表示单词所需的内存,使我们的系统变得低效。

  • 像这种独热编码表示无法捕捉到单词之间的相似性。例如,在句子中有两个单词,“like”“love”。我们知道“like”“love”更接近,而比“Football”要相似。然而,在当前的独热编码表示中,任何两个向量的点积都是零。数学上,“like”“Football”的点积如下表示:

"like" * "Football" = 转置([0,1,0]) [0,0,1] = 00 + 10 + 01 = 0

这是因为我们为每个单词在向量中分配了一个独立的位置,并且该位置只有一个 1。

对于垃圾邮件检测和语言翻译问题,理解单词之间的相似性非常重要。为此,有几种方法(包括监督学习和无监督学习)可以让我们学习词嵌入。

你可以通过这个官方教程了解如何使用 TensorFlow 的词嵌入。

本项目使用 Keras 的嵌入层将我们的电影评论中的单词映射到实际值的向量表示中。在这个项目中,我们通过监督学习的方式来学习单词的向量表示。基本上,我们随机初始化词嵌入,然后通过神经网络中的反向传播来更新嵌入,以最小化总的网络损失。以监督的方式训练它们有助于生成任务特定的嵌入。例如,我们期望AwesomeGreat这样的单词具有相似的表示,因为它们都表示积极的情感。一旦我们对电影评论中的单词进行了编码,就可以将它们作为输入传递到神经网络层中。

构建情感分析模型

在这一部分,我们将学习如何使用 Keras 从头开始构建情感分析模型。为了进行情感分析,我们将使用来自密歇根大学的情感分析数据,数据可以在www.kaggle.com/c/si650winter11/data找到。这个数据集包含了 7,086 条带标签的电影评论。标签1表示积极情感,而0表示消极情感。在这个代码库中,数据集保存在名为sentiment.txt的文件中。

数据预处理

一旦你安装了运行此项目并读取数据所需的包(可以在requirements.txt文件中找到),下一步就是对数据进行预处理:

  1. 第一步是从评论中获取词元/单词列表。去除任何标点符号,并确保所有词元都是小写字母:
def get_processed_tokens(text):
'''
Gets Token List from a Review
'''
filtered_text = re.sub(r'[^a-zA-Z0-9\s]', '', text) #Removing Punctuations
filtered_text = filtered_text.split()
filtered_text = [token.lower() for token in filtered_text]
return filtered_text

例如,如果我们输入This is a GREAT movie!!!!,那么输出应该是this is a great movie

  1. 创建一个token_idx字典,将词汇映射到整数,以创建嵌入。请注意,字典中可能包含非常多的独特词汇(tokens),因此我们必须过滤掉在训练集中出现次数少于阈值的词汇(代码中默认值为5)。这是因为学习电影情感和在数据集中出现频率较低的词汇之间的关系非常困难:
def tokenize_text(data_text, min_frequency =5):
    '''
    Tokenizes the reviews in the dataset. Filters non frequent tokens
    '''
    review_tokens = [get_processed_tokens(review) for review in   
                     data_text] # Tokenize the sentences
    token_list = [token for review in review_tokens for token in review]         
    #Convert to single list
    token_freq_dict = {token:token_list.count(token) for token in     
    set(token_list)} # Get the frequency count of tokens
    most_freq_tokens = [tokens for tokens in token_freq_dict if 
    token_freq_dict[tokens] >= min_frequency]
    idx = range(len(most_freq_tokens))
    token_idx = dict(zip(most_freq_tokens, idx))
    return token_idx,len(most_freq_tokens)
  1. 将数据集中的每个评论映射到一个整数序列(基于我们在上一步创建的token_idx字典)。但是,在执行此操作之前,首先找到具有最多词汇的评论:
def get_max(data):
    '''
    Get max length of the token
    '''
    tokens_per_review = [len(txt.split()) for txt in data]
    return max(tokens_per_review)
  1. 为了创建输入模型学习嵌入的序列,我们必须为数据集中的每个评论创建一个固定长度的(max_tokens)序列。如果评论的长度小于最大长度,我们将用零进行前填充,以确保所有序列的长度一致。与后填充相比,前填充被认为能帮助获得更准确的结果:

def create_sequences(data_text,token_idx,max_tokens):
    '''
    Create sequences appropriate for GRU input
    Input: reviews data, token dict, max_tokens
    Output: padded_sequences of shape (len(data_text), max_tokens)
    '''
    review_tokens = [get_processed_tokens(review) for review in  
                   data_text] # Tokenize the sentences 
    #Covert the tokens to their indexes 
    review_token_idx = map( lambda review: [token_idx[k] for k in review 
                           if k in token_idx.keys() ], review_tokens)
    padded_sequences = pad_sequences(review_token_idx,maxlen=max_tokens)
    return np.array(padded_sequences)

构建模型

该模型将包括一个嵌入层,接着是三层 GRU 和一个带有 sigmoid 激活函数的全连接层。对于优化和准确度评估,我们将分别使用Adam优化器和binary_crossentropy损失函数:

  1. 模型使用以下参数定义:
def define_model(num_tokens,max_tokens):
    '''
    Defines the model definition based on input parameters
    '''
    model = Sequential()
    model.add(Embedding(input_dim=num_tokens,
                    output_dim=EMBEDDING_SIZE,
                    input_length=max_tokens,
                    name='layer_embedding'))

    model.add(GRU(units=16, name = "gru_1",return_sequences=True))
    model.add(GRU(units=8, name = "gru_2",return_sequences=True))
    model.add(GRU(units=4, name= "gru_3"))
    model.add(Dense(1, activation='sigmoid',name="dense_1"))
    optimizer = Adam(lr=1e-3)
    model.compile(loss='binary_crossentropy',
                  optimizer=optimizer,
                  metrics=['accuracy'])
    print model.summary()
    return model
  1. 使用以下参数训练模型:

    • 训练轮次 = 15

    • 验证分割 = 0.05

    • 批量大小 = 32

    • 嵌入大小 = 8

def train_model(model,input_sequences,y_train):
    '''
    Train the model based on input parameters
    '''

    model.fit(input_sequences, y_train,
          validation_split=VAL_SPLIT, epochs=EPOCHS, 
          batch_size=BATCH_SIZE)
    return model
  1. 测试在一些随机评论句子上训练的模型,以验证其性能:
文本 预测得分
很棒的电影 0.9957
糟糕的电影 0.0023
那部电影真糟糕 0.0021
我喜欢那部电影 0.9469

预测得分对于正面句子接近 1,对于负面句子接近 0。这验证了我们对模型性能的随机检查。

请注意,如果你在不同的硬件上训练模型,实际得分可能会略有不同。

在浏览器中使用 TensorFlow.js 运行模型

在这一部分,我们将把模型部署到浏览器上。

以下步骤演示了如何保存模型:

  1. 安装 TensorFlow.js,它将帮助我们按照浏览器可使用的格式来转换训练好的模型:
pip install tensorflowjs
  1. 将模型保存为 TensorFlow.js 格式:
import tensorflowjs as tfjs 
tfjs.converters.save_keras_model(model, OUTPUT_DIR)

这将创建一个名为model.json的 json 文件,该文件将包含元变量和一些其他文件,例如group1-shard1of1

做得好!不过,将模型部署到 HTML 文件中稍微有点复杂:

若要运行库中提到的代码,请仔细遵循README.md文档中的说明(如有必要,请注意故障排除部分),确保在运行Run_On_Browser.html文件之前正确设置。

  1. 通过脚本标签将 TensorFlow.js 集成到你的 JavaScript 中:
<script src="img/tfjs@0.8.0"></script>
  1. 加载模型和我们的token_idx字典。这将帮助我们在处理任何来自浏览器的评论之前,加载相关数据:
async function createModel()
{
const model = await
tf.loadModel('http://127.0.0.1:8000/model.json')
return model
}
async function loadDict()
{
 await $.ajax({
 url: 'http://127.0.0.1:8000/token_index.csv',
 dataType: 'text',
 crossDomain : true}).done(success);
}
function success(data)
{
    var wd_idx = new Object();
    lst = data.split(/\r?\n|\r/)
    for(var i = 0 ; i < lst.length ;i++){
        key = (lst[i]).split(',')[0]
        value = (lst[i]).split(',')[1]

        if(key == "")
            continue
        wd_idx[key] = parseInt(value) 
    }

    word_index = wd_idx
}

async function init()
{
 word_index = undefined
 console.log('Start loading dictionary')
 await loadDict()
 //console.log(word_index)
 console.log('Finish loading dictionary')
 console.log('Start loading model') 
 model = await createModel()
 console.log('Finish loading model') 
}
  1. 添加一些辅助函数来处理浏览器中的评论输入。这包括处理文本、将单词映射到token_idx,并为模型预测创建序列:
function process(txt)
{
 out = txt.replace(/[^a-zA-Z0-9\s]/, '')
 out = out.trim().split(/\s+/)
 for (var i = 0 ; i < out.length ; i++)
 out[i] = out[i].toLowerCase()
 return out
}

function create_sequences(txt)
{
 max_tokens = 40 
 tokens = []
 words = process(txt)
 seq = Array.from(Array(max_tokens), () => 0) 
 start = max_tokens-words.length
 for(var i= 0 ; i< words.length ; i++)
 {
     if (Object.keys(word_index).includes(words[i])){
         seq[i+start] = word_index[words[i]]
     } 
 }
 return seq
}
  1. 集成预测函数,处理输入句子并使用模型的预测函数返回一个包含预测得分的张量,正如上一节所示:
async function predict()
{
 txt = document.getElementById("userInput").value
 alert(txt);
 seq = create_sequences(txt) 
 input = tf.tensor(seq)
 input = input.expandDims(0)
 pred = model.predict(input)
 document.getElementById("Sentiment").innerHTML = pred;

 pred.print()
}
  1. 为了从用户的角度说明整个过程,打开Run_on_Browser.html文件。你会看到类似下面截图中的内容:

截图的左侧表示网站的布局,而右侧显示了控制台和输出内容。

请注意,我们可以提前加载字典和模型,以加速预测。

  1. 在提供的框中输入评论并点击提交按钮,以查看模型的预测得分。尝试用Awesome Movie文本运行应用程序:

预测得分相当高,表明情感是积极的。你可以尝试不同的文本来看结果。

请注意,这主要是为了演示目的,如果你愿意,你可以通过 JavaScript 改进用户界面。

总结

本章简要介绍了如何构建一个端到端系统,使用 Keras 训练情感分析模型并通过 TensorFlow.js 将其部署到 JavaScript 中。将模型部署到生产环境中的过程非常顺畅。

接下来的一个潜在步骤是修改 JavaScript,以便在输入单词时立即预测情感。正如我们之前提到的,通过使用 TensorFlow.js 部署模型,你可以启用低延迟应用,如实时情感预测,而无需与服务器交互。

最后,我们在 Python 中构建了一个神经网络并将其部署在 JavaScript 中。不过,你也可以尝试使用 TensorFlow.js 在 JavaScript 中构建整个模型。

在下一章,我们将了解谷歌的新库——TensorFlow Lite。

问题

  1. 如果使用 LSTM 代替 GRU,你能评估模型的准确性吗?

  2. 如果你增加了嵌入大小(Embedding Size),会对准确性和训练时间产生什么影响?

  3. 你能在模型中加入更多层吗?训练时间会发生什么变化?

  4. 你能修改代码,以便在浏览器内训练模型,而不是加载已训练的模型吗?

第四章:使用 TensorFlow Lite 进行数字分类

过去五年,机器学习ML)领域取得了很大进展。如今,许多 ML 应用已经进入我们的日常生活,而我们往往没有意识到。由于 ML 已成为焦点,如果我们能在移动设备上运行深度模型,那将非常有帮助,而移动设备正是我们日常生活中最常使用的设备之一。

移动硬件的创新,加上用于在移动设备上部署 ML 模型的新软件框架,正在成为开发基于 ML 的移动应用或其他边缘设备(如平板电脑)应用的主要推动力之一。

在本章中,我们将学习谷歌的新库 TensorFlow Lite,该库可以用于在移动设备上部署 ML 模型。我们将使用 MNIST 数字数据集训练一个深度学习模型,并通过理解以下概念来了解如何将该模型转换为适合移动设备的格式:

  • TensorFlow Lite 及其架构简介

  • 分类模型评估指标简介

  • 在 MNIST 数据集上开发深度学习模型

  • 使用 TensorFlow Lite 将训练好的模型转换为适合移动设备的格式

请注意,本章不会讨论如何构建 Android 应用来部署这些模型,因为这一内容在谷歌的 TensorFlow 教程中已有广泛文档记录(www.tensorflow.org/lite/)。

什么是 TensorFlow Lite?

在深入探讨 TensorFlow Lite 之前,让我们先了解在边缘设备(如手机、平板电脑等)上进行 ML 的优势。

  • 隐私:如果 ML 模型的推理可以在设备上进行,用户数据就不需要离开设备,从而有助于保护用户隐私。

  • 离线预测:设备无需连接网络即可对 ML 模型进行预测。这为像印度这样的开发中国家提供了大量应用场景,因为这些地区的网络连接状况并不理想。

  • 智能设备:这也可以促进智能家居设备的发展,如带有设备智能的微波炉和恒温器。

  • 节能:设备上的 ML 推理可以更节能,因为无需将数据来回传输到服务器。

  • 传感器数据利用:ML 模型可以利用丰富的传感器数据,因为它在移动设备上易于获取。

然而,移动设备与我们的桌面和笔记本电脑不同。在将模型部署到移动或嵌入式设备时,需要考虑不同的因素,比如:

  • 模型大小:如我们所知,移动设备的内存有限,我们不能在设备上存储占用大量内存的模型。处理这个问题有两种方式:

    • 我们可以对模型的权重进行四舍五入或量化,使其所需的浮点表示更少。这与我们的理解一致,即整数存储所需的内存通常少于浮点数。

    • 由于我们仅在设备上进行推断或预测,因此可以去除 Tensorflow 图中所有对进行预测无用的训练操作。

  • 速度:在移动设备上部署模型的重要因素之一是我们能够运行推断的速度,以获得更好的用户体验。必须优化模型,以确保它们不会超出手机的延迟预算,同时又保持快速。

  • 部署简便性:我们需要高效的框架/库,以便在移动设备上部署非常简单。

考虑到这些因素,Google 开发了 TensorFlow Lite,这是原始 TensorFlow 的轻量级版本,用于在移动和嵌入式设备上部署深度学习模型。

要了解 TensorFlow Lite,请查看以下显示其高级架构的图表:

这种架构清楚地表明,我们需要将训练好的 TF 模型转换为.tflite格式。这种格式与通常的 TF 模型不同,因为它经过优化,可用于设备上的推断。我们将在本章后面详细学习转换过程。

现在,让我们试着了解使用 TF Lite 格式的主要特点:

  • 模型被序列化并转换为 Flatbuffer 格式(google.github.io/flatbuffers/)。Flatbuffers 的优点在于数据可以直接访问,无需解析/解包包含权重的大文件。

  • 模型的权重和偏差已预先融合到 TF Lite 格式中。

TF Lite 跨平台,可部署在 Android、iOS、Linux 和硬件设备(如 Raspberry Pi)上。

它包括一个在设备上优化为更快执行的解释器。所有支持的操作的核心解释器大小约为 400 KB,不支持操作时为 75 KB。这意味着模型在设备上占用的空间很小。总体而言,理念是保留仅对推断至关重要的模型部分,并剥离所有其他部分。

利用硬件创新,许多公司还在开发专为神经网络推断优化的 GPU 和数字信号处理器(DSP)。TF Lite 提供了 Android 神经网络 API,可以在这些设备上进行硬件加速。

分类模型评估指标

仅仅构建模型是不够的;我们需要确保我们的模型功能良好,并为我们提供良好和准确的输出。为此,我们需要了解一些分类指标,这些指标将用于全书中评估模型。

让我们从定义一些用于评估分类模型的度量标准开始。为此,取一个简单的垃圾邮件检测示例作为参考,任何在线邮箱都可以进行这样的检测。垃圾邮件将被视为正类,正常邮件则视为负类。我们可以将这个垃圾邮件检测模型总结为四类,如下矩阵所示:

真正类 (TP) 假正类 (FP)
现实:邮件是垃圾邮件 现实:邮件不是垃圾邮件
模型预测:邮件是垃圾邮件 模型预测:邮件是垃圾邮件
假负类 (FN) 真负类 (TN)
现实:邮件是垃圾邮件 现实:邮件不是垃圾邮件
模型预测:邮件不是垃圾邮件 模型预测:邮件不是垃圾邮件

这个矩阵通常也被称为 混淆矩阵

我们将用来定义分类器质量的三大主要度量标准,主要针对不平衡数据集,分别如下:

  • 准确度:准确度是用于分类问题的最基本度量标准。其定义如下:

  • 精确度:精确度试图衡量模型预测出的所有正类中真正的正类数量。如果你的 Gmail 没有将很多来自朋友(或正常邮件)的邮件错误分类到垃圾邮件中,那么它的精确度就非常高。其数学表示如下:

  • 召回率:召回率试图衡量数据集中所有真实正类中被分类为正类的数量。简单来说,如果你的 Gmail 没有将很多垃圾邮件误分类为正常邮件并将其发送到收件箱,那么它的召回率就非常高:

理想情况下,我们希望模型具有高精确度和高召回率。然而,在机器学习中,精确度和召回率之间总是存在权衡。

使用 TensorFlow Lite 分类数字

为了完成这个项目,我们将使用 MNIST 手写数字数据集,该数据集可在 TensorFlow 数据集库中找到(www.tensorflow.org/guide/datasets)。它包含了从 0 到 9 的手写数字图像。训练数据集有 60,000 张图像,测试集有 10,000 张图像。数据集中的一些图像如下所示:

如果我们查看 TensorFlow Lite 教程,会发现重点是使用预训练模型,如 Mobilenet 或重新训练现有的模型。然而,这些教程中没有谈到构建新模型,而这正是我们在这里要做的事情。

请注意,我们特别选择了一个简单的模型,因为在撰写本书时,TensorFlow Lite 对所有复杂模型的支持不足。

我们将使用类别交叉熵作为这个分类问题的损失函数。类别交叉熵在本书的第三章《使用 TensorFlow.js 在浏览器中进行情感分析》中有详细说明。在本章中,我们的数据集包含 10 个不同的数字,因此我们将使用类别交叉熵,分类数为 10。

数据预处理与模型定义

我们需要对数据进行预处理,以使其准备好输入到模型中,定义我们的模型,并创建一个评估指标:

  1. 通过确保图像的形状为 28x28x1,并将像素转换为浮动类型变量来进行数据预处理,以供训练使用。同时,这里我们定义 NUM_CLASSES = 10,因为图像中有 10 个不同的数字。
x_train = x_train.reshape(x_train.shape[0], IMAGE_SIZE, IMAGE_SIZE, 1)
x_test = x_test.reshape(x_test.shape[0], IMAGE_SIZE, IMAGE_SIZE, 1)
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
Next, we normalize the image pixels by 255 as follows:
x_train /= 255
x_test /= 255
And finally, we convert the class labels to one hot for training as follows:
y_train = keras.utils.to_categorical(y_train, NUM_CLASSES)
y_test = keras.utils.to_categorical(y_test, NUM_CLASSES)

  1. 定义模型为具有两个卷积层(使用相同的过滤器大小)、两个全连接层、两个丢弃层(丢弃概率分别为 0.25 和 0.5)、每个全连接层或卷积层后都有一个修正线性单元(ReLU,除最后一层外),以及一个最大池化层。此外,我们还添加了 Softmax 激活函数,将模型的输出转化为每个数字的概率。请注意,我们使用此模型是因为它能产生良好的结果。你可以通过添加更多层或尝试不同形状的现有层来改进该模型。
model = Sequential()
model.add(Conv2D(32, kernel_size=(3, 3),
activation='relu',
input_shape=INPUT_SHAPE))
model.add(Conv2D(64, (3, 3), activation='relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(128, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(NUM_CLASSES))model.add(Activation('softmax', name = 'softmax_tensor'))

注意,我们将输出张量命名为 softmax_tensor,这将在我们尝试将此模型转换为 TensorFlow Lite 格式时非常有用。

  1. 进一步为模型定义以下参数:

    • 损失 = 类别交叉熵

    • 优化器 = AdaDelta。Adam 优化器在第三章《使用 TensorFlow.js 在浏览器中进行情感分析》中有介绍,它是 AdaDelta 的扩展。我们使用 AdaDelta,因为它在这个模型上能够获得良好的结果。你可以在原始论文中找到更多关于 AdaDelta 的细节 (arxiv.org/abs/1212.5701)。

    • 评估指标 = 分类准确度

定义这些内容的代码如下:

model.compile(loss=keras.losses.categorical_crossentropy,

optimizer=keras.optimizers.Adadelta(),

metrics=['accuracy'])
  1. 启用 TensorBoard 日志记录,以可视化模型图和训练进度。代码定义如下:
tensorboard = TensorBoard(log_dir=MODEL_DIR)
  1. 使用以下参数训练模型:

    • 训练周期 = 12

    • 批量大小 = 128:

self.model.fit(self.x_train, self.y_train,
batch_size=BATCH_SIZE,

epochs=EPOCHS,

verbose=1,

validation_data=(self.x_test, self.y_test),

callbacks = [self.tensorboard])

score = self.model.evaluate(self.x_test, self.y_test, verbose=0)

我们在测试数据集上仅用 12 个训练周期达到了 99.24% 的准确率。

注意,我们使用 callbacks 参数来记录 TensorBoard 上的训练进度。

将 TensorFlow 模型转换为 TensorFlow Lite

既然我们已经按照常规方法训练了模型,接下来我们来看看如何将该模型转换为 TensorFlow Lite 格式。

转换的通用流程如下面的图示所示:

该过程很简单:我们获取一个已训练的模型,冻结图形,优化它以进行推理/预测,并将其转换为 .tflite 格式。在继续之前,让我们先了解一下“冻结图形”和“优化推理”的含义:

  • 冻结图:冻结图操作通过将所有 TF 变量转换为常量,从而有效地冻结了模型的权重。正如你所想的,将所有权重作为常量可以节省空间,相比保持它们作为变量。在移动设备上我们只进行推理(而不是训练),所以我们不需要修改模型权重。

  • 优化推理:一旦图被冻结,我们会移除图中所有对推理无用的操作。例如,Dropout 操作用于训练模型,以防过拟合。然而,在移动设备上的预测过程中,这个操作完全没有用处。

在本节的剩余部分,我们将大量使用 TensorBoard 可视化(www.TensorFlow.org/guide/summaries_and_tensorboard)来进行图形可视化。

  1. 一旦你训练了模型,你的模型文件夹中必须有一个以events.out.tfevents.为前缀的文件。进入logs文件夹并在终端中输入以下命令:
tensorboard --logdir <model_folder>

默认情况下,TensorBoard 将启动在6006端口。通过进入浏览器并在地址栏中输入localhost:6006启动它。一旦打开 TensorBoard,如果你导航到顶部的 Graph 标签,你将能够看到你模型的 Tensorflow 图。在以下示意图中,我们展示了主图,并标注了输入张量、输出张量以及图中的训练部分。正如我们所看到的,我们不应该保留任何用于训练图的内容,因为它们对在移动设备上进行推理/预测没有任何用处。

  1. 接下来实现一个名为freeze_sesssion的函数,该函数接受 TF 会话作为输入,将所有变量转换为常量,并返回冻结的图。执行此函数后,你将在<model_folder>/logs/freeze文件夹中获得一个名为MNIST_model.pb的冻结图文件。
from TensorFlow.python.framework.graph_util import convert_variables_to_constants

def freeze_session(session, keep_var_names=None, output_names=None, clear_devices=True):

graph = session.graph

with graph.as_default():

freeze_var_names = list(set(v.op.name for v in tf.global_variables()).difference(keep_var_names or []))

output_names = output_names or []

output_names += [v.op.name for v in tf.global_variables()]

input_graph_def = graph.as_graph_def()

if clear_devices:

for node in input_graph_def.node:

node.device = ""

frozen_graph = convert_variables_to_constants(session, input_graph_def,

output_names, freeze_var_names)

return frozen_graph

  1. 现在,事情变得有些奇怪:你不能通过 TensorBoard 直接可视化MNIST_model.pb文件。你需要将图写成 TensorBoard 能够识别的格式。执行下面提到的pb_to_tensorboard函数,你将会在<model_folder>/logs/freeze文件夹中看到一个以events.out.tfevents为前缀的文件。

def pb_to_tensorboard(input_graph_dir,graph_type ="freeze"):

  file_name = ""

  if graph_type == "freeze":

      file_name = FREEZE_FILE_NAME

  elif graph_type == "optimize":

      file_name = OPTIMIZE_FILE_NAME

  with tf.Session() as sess:

      model_filename = input_graph_dir + "/" + file_name

      with gfile.FastGFile(model_filename, 'rb') as f:

           graph_def = tf.GraphDef()

           graph_def.ParseFromString(f.read())

  train_writer = tf.summary.FileWriter(input_graph_dir)

  train_writer.add_graph(sess.graph)

  1. 接下来,使用<model_folder>/logs/freeze作为logdir重新启动 TensorBoard,并可视化冻结的图。你会看到,图中大多数变量已经被去除。以下示意图展示了你将获得的冻结图:

  1. 下一步是进一步优化图形以便推理。如前所述,我们将从图中移除 Dropout 变量,因为它们对移动设备上的推理没有用。然而,根据现有的 TensorFlow 函数/程序,没有完美的方法来移除这些操作。TensorFlow Lite 的新改进并不适用于本示例,这表明它们仍在开发中。相反,您必须手动指定要移除的操作,并将 Dropout 操作的输入连接到图中它们之后的操作。例如,在冻结的图中,假设我们要移除dropout_3操作。下图显示了冻结图的放大版:

在这种情况下,您需要将max_pooling2操作直接连接到flatten_2操作,从而跳过图中的dropout_3操作。

执行下文提到的optimize_graph函数,以去除图中的所有 Dropout 操作。它会手动将所有 Dropout 操作从图中清除。这将导致在<model_folder>/logs/optimized文件夹下生成一个名为MNIST_optimized.pb的新文件。

def optimize_graph(input_dir, output_dir):
input_graph = os.path.join(input_dir, FREEZE_FILE_NAME)
output_graph = os.path.join(output_dir, OPTIMIZE_FILE_NAME)
input_graph_def = tf.GraphDef()
with tf.gfile.FastGFile(input_graph, "rb") as f:
input_graph_def.ParseFromString(f.read())
output_graph_def = strip(input_graph_def, u'dropout_1', u'conv2d_2/bias', u'dense_1/kernel', u'training')
output_graph_def = strip(output_graph_def, u'dropout_3', u'max_pooling2d_2/MaxPool', u'flatten_2/Shape',
u'training')
output_graph_def = strip(output_graph_def, u'dropout_4', u'dense_3/Relu', u'dense_4/kernel', u'training')
output_graph_def = strip(output_graph_def, u'Adadelta_1', u'softmax_tensor_1/Softmax',
u'training/Adadelta/Variable', u'training')
output_graph_def = strip(output_graph_def, u'training', u'softmax_tensor_1/Softmax',
u'_', u'training')
with tf.gfile.GFile(output_graph, "wb") as f:
f.write(output_graph_def.SerializeToString())
  1. 同样,为了在 TensorBoard 中可视化图形,您需要使用步骤 3 中定义的函数pb_to_tensorboar进行转换,以使其适应 TensorBoard,并在同一文件夹中获得一个以events.out.tfevents为前缀的新文件。下图展示了去除 Dropout 操作后您将获得的图形。

请注意,从图中去除 Dropout 不会影响测试集的准确性,因为 Dropout 并未用于推理。

  1. 获取移动友好格式的模型的最后一步是将其转换为 .tflite 文件。对于此步骤,您将使用toco命令,该命令代表 TensorFlow Lite 优化转换器(www.tensorflow.org/lite/convert/)。以下是提供的代码:
toco \
--input_file=<model_folder>/logs/optimized/MNIST_optimized.pb\
--input_format=TensorFlow_GRAPHDEF \
--output_format=TFLITE \
--inference_type=FLOAT \
--input_type=FLOAT \
--input_arrays=conv2d_1_input \
--output_arrays=softmax_tensor_1/Softmax \
--input_shapes=1,28,28,1 \
--output_file=<model_folder>//mnist.tflite

这将生成一个名为mnist.tflite的文件,保存在<model_folder>中。实质上,此步骤是将优化后的图形转换为 Flatbuffer,以便于高效的设备端推理。

我们不会涵盖将我们的项目部署到移动设备上的内容,因为该部分开发超出了本书的范围。然而,您可以查看 TensorFlow Lite 的教程,了解如何将 TF Lite 模型部署到 Android(www.tensorflow.org/lite/demo_android)或 iOS(www.tensorflow.org/lite/demo_ios)。

总结

机器学习正处于下一波发展的前沿,我们正在努力将机器学习普及到日常生活中。它具有诸多优势,例如离线访问、数据隐私等。

在本章中,我们介绍了来自谷歌的一个新库——TensorFlow Lite,它已针对在移动设备和嵌入式设备上部署机器学习模型进行了优化。我们了解了 TensorFlow Lite 的架构,它将训练好的 TensorFlow 模型转换为 .tflite 格式。这是为在设备上进行快速推理和低内存占用而设计的。TensorFlow Lite 还支持多个平台,如 Android、iOS、Linux 和 Raspberry Pi。

接下来,我们使用了 MNIST 手写数字数据集来训练一个深度学习模型。随后,我们按照必要的步骤将训练好的模型转换为 .tflite 格式。步骤如下:

  1. 将图冻结,将变量转换为常量

  2. 通过移除未使用的操作(如 Dropout),优化了推理图

  3. 使用 TensorFlow 优化转换工具 (toco) 将优化后的模型转换为 .tflite 格式

在每一步,我们都使用 TensorBoard 来可视化图的状态。

这是一个非常激动人心的领域,正在不断发展,无论是在硬件还是软件方面。一旦这项技术成熟,它将在全球范围内开辟新的使用场景和商业模式。

在下一章中,我们将创建一个项目,帮助我们将文本转换为语音。

问题

以下是一些问题:

  1. TensorFlow Lite 与常规 TensorFlow 有什么不同?

  2. 你能尝试在第三章中使用电影评论数据集构建模型吗?在浏览器中使用 TensorFlow.js 进行情感分析?如果是这样,你在使用 TensorFlow Lite 时遇到什么问题了吗?

  3. 你能尝试使用 Adam 优化器,看看它是否能提升模型的性能吗?

  4. 你能想到除了 Dropout 之外,也不重要于移动端推理的操作吗?

第五章:语音转文本与主题提取使用自然语言处理

由于语音数据的复杂性和多样性,识别和理解口语语言是一个具有挑战性的问题。过去已经部署了几种不同的技术来识别口语单词。大多数方法的适用范围非常有限,因为它们无法识别各种单词、口音和语调,以及口语语言的某些方面,如单词之间的停顿。一些常见的语音识别建模技术包括隐马尔可夫模型HMM)、动态时间规整DTW)、长短期记忆网络LSTM)和连接时序分类CTC)。

本章将介绍语音转文本的各种选项,以及 Google 的 TensorFlow 团队使用语音命令数据集的预构建模型。我们将讨论以下主题:

  • 语音转文本框架和工具包

  • Google 语音命令数据集

  • 基于卷积神经网络的语音识别架构

  • 一个 TensorFlow 语音命令示例

下载并遵循本章的代码:github.com/tensorflow/tensorflow/blob/master/tensorflow/examples/speech_commands/

语音转文本框架和工具包

许多基于云的 AI 提供商提供语音转文本作为服务:

  • 亚马逊提供的语音识别服务被称为Amazon Transcribe。Amazon Transcribe 支持将存储在 Amazon S3 中的音频文件转录为四种不同格式:.flac.wav.mp4.mp3。它允许最大长度为两小时,最大大小为 1 GB 的音频文件。转录结果以 JSON 文件格式保存在 Amazon S3 存储桶中。

  • Google 将语音转文本作为其 Google Cloud ML 服务的一部分提供。Google Cloud Speech to Text 支持 FLACLinear16MULAWAMRAMR_WBOGG_OPUS 文件格式。

  • 微软在其 Azure 认知服务平台中提供语音转文本 API,称为语音服务 SDK。语音服务 SDK 与其他 Microsoft API 集成,用于转录录制的音频。它仅支持单声道 WAV 或 PCM 文件格式,且采样率为 6 kHz。

  • IBM 在其 Watson 平台中提供语音转文本 API。Watson Speech to Text 支持八种音频格式:BASIC、FLAC、L16、MP3、MULAW、OGG、WAV 和 WEBM。音频文件的最大大小和时长根据使用的格式而异。转录结果以 JSON 文件返回。

除了对各种国际口语语言和广泛的全球词汇表的支持外,这些云服务还在不同程度上支持以下功能:

  • 多通道识别:识别多个通道中记录的多个参与者

  • 说话者分离:预测特定说话者的语音

  • 自定义模型和模型选择:插入您自己的模型并从大量预构建模型中选择

  • 不当内容过滤和噪声过滤

还有许多用于语音识别的开源工具包,如 Kaldi。

Kaldi (http:/kaldi-asr.org) 是一个流行的开源语音转文本识别库。它是用 C++编写的,且可以从github.com/kaldi-asr/kaldi获取。Kaldi 可以通过其 C++ API 集成到您的应用程序中,也支持使用 NDK、clang++和 OpenBLAS 在 Android 上运行。

Google 语音命令数据集

Google 语音命令数据集由 TensorFlow 和 AIY 团队创建,旨在展示使用 TensorFlow API 的语音识别示例。该数据集包含 65,000 个一秒钟长的音频片段,每个片段包含由成千上万的不同发音者所说的 30 个不同单词之一。

Google 语音命令数据集可以从以下链接下载:download.tensorflow.org/data/speech_commands_v0.02.tar.gz

这些音频片段是在真实环境中使用手机和笔记本电脑录制的。35 个单词包含噪声词,另外 10 个命令词是机器人环境中最有用的,列举如下:

  • 是的

  • 向上

  • 向下

  • 向左

  • 向右

  • 向前

  • 关闭

  • 停止

  • 继续

关于如何准备语音数据集的更多细节,请参见以下链接:

使用这个数据集,因此,本章中的示例问题被称为关键字检测任务。

神经网络架构

该示例使用的网络包含三个模块:

  • 一个特征提取模块,将音频片段处理为特征向量

  • 一个深度神经网络模块,为输入特征向量帧中的每个单词生成软最大概率

  • 一个后验处理模块,将帧级后验分数合并成每个关键字的单一分数

特征提取模块

为了简化计算,传入的音频信号通过语音活动检测系统,信号被划分为语音部分和非语音部分。语音活动检测器使用一个 30 分量对角协方差 GMM 模型。该模型的输入是 13 维 PLP 特征、其增量和二阶增量。GMM 的输出传递给一个状态机进行时间平滑处理。

该 GMM-SM 模块的输出是信号中的语音部分和非语音部分。

信号中的语音部分进一步处理以生成特征。声学特征基于 40 维对数滤波器组能量计算,每 10 毫秒计算一次,窗口为 25 毫秒。信号中还加入了 10 个未来帧和 30 个过去帧。

关于特征提取器的更多细节可以从原始论文中获得,相关链接在进一步阅读部分提供。

深度神经网络模块

DNN 模块是使用卷积神经网络(CNN)架构实现的。代码实现了多种不同变体的 ConvNet,每个变体产生不同的准确度,并且训练所需的时间不同。

构建模型的代码提供在 models.py 文件中。它允许根据命令行传递的参数创建四种不同的模型:

  • single_fc:该模型仅有一个全连接层。

  • conv:该模型是一个完整的 CNN 架构,包含两对卷积层和最大池化层,后跟一个全连接层。

  • low_latency_conv:该模型有一个卷积层,后面跟着三个全连接层。顾名思义,与 conv 架构相比,它的参数和计算量较少。

  • low_latency_svdf:该模型遵循论文《压缩深度神经网络》中的架构和层。

    使用秩约束拓扑的网络 可从 research.google.com/pubs/archive/43813.pdf 获得。

  • tiny_conv:该模型只有一个卷积层和一个全连接层。

如果命令行未传递架构,则默认架构为 conv。在我们的运行中,架构在使用默认准确率和默认步数 18,000 训练模型时,显示了以下训练、验证和测试集的准确率:

架构 准确率(%)
训练集 验证集
conv(默认) 90
single_fc 50
low_latenxy_conv 22
low_latency_svdf 7
tiny_conv 55

由于网络架构使用的是更适合图像数据的 CNN 层,因此语音文件通过将短时间段的音频信号转换为频率强度的向量,转化为单通道图像。

从前面的观察可以看出,缩短的架构在相同超参数下给出的准确率较低,但运行速度更快。因此,可以运行更多的迭代轮次,或者可以增加学习率以获得更高的准确率。

现在让我们来看一下如何训练和使用这个模型。

训练模型

  1. 移动到从仓库克隆代码的文件夹,并使用以下命令训练模型:
python tensorflow/examples/speech_commands/train.py

你将开始看到训练的输出,如下所示:

I tensorflow/core/platform/cpu_feature_guard.cc:141] Your CPU supports instructions that this TensorFlow binary was not compiled to use: SSE4.1 SSE4.2 AVX AVX2 FMA
I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:897] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
I tensorflow/core/common_runtime/gpu/gpu_device.cc:1405] Found device 0 with properties:
name: Quadro P5000 major: 6 minor: 1 memoryClockRate(GHz): 1.506
pciBusID: 0000:01:00.0
totalMemory: 15.90GiB freeMemory: 14.63GiB
I tensorflow/core/common_runtime/gpu/gpu_device.cc:1484] Adding visible gpu devices: 0
I tensorflow/core/common_runtime/gpu/gpu_device.cc:965] Device interconnect StreamExecutor with strength 1 edge matrix:
I tensorflow/core/common_runtime/gpu/gpu_device.cc:971] 0
I tensorflow/core/common_runtime/gpu/gpu_device.cc:984] 0: N
I tensorflow/core/common_runtime/gpu/gpu_device.cc:1097] Created TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with 14168 MB memory) -> physical GPU (device: 0, name: Quadro P5000, pci bus id: 0000:01:00.0, compute capability: 6.1)
  1. 一旦训练迭代开始,代码会打印出学习率、训练集的准确率以及交叉熵损失,如下所示:
INFO:tensorflow:Training from step: 1
INFO:tensorflow:Step #1: rate 0.001000, accuracy 12.0%, cross entropy 2.662751
INFO:tensorflow:Step #2: rate 0.001000, accuracy 6.0%, cross entropy 2.572391
INFO:tensorflow:Step #3: rate 0.001000, accuracy 11.0%, cross entropy 2.547692
INFO:tensorflow:Step #4: rate 0.001000, accuracy 8.0%, cross entropy 2.615582
INFO:tensorflow:Step #5: rate 0.001000, accuracy 5.0%, cross entropy 2.592372
  1. 代码还会每 100 步保存一次模型,因此如果训练中断,可以从最近的检查点重新开始:
INFO:tensorflow:Saving to "/tmp/speech_commands_train/conv.ckpt-100"

训练运行了几个小时,共 18,000 步,最终会打印出最终的训练学习率、准确度、损失和混淆矩阵,如下所示:

INFO:tensorflow:Step #18000: rate 0.000100, accuracy 90.0%, cross entropy 0.420554
INFO:tensorflow:Confusion Matrix:
 [[368 2 0 0 1 0 0 0 0 0 0 0]
 [ 3 252 9 6 13 15 13 18 17 1 13 11]
 [ 0 1 370 12 2 2 7 2 0 0 0 1]
 [ 3 8 4 351 8 7 6 0 0 0 3 16]
 [ 3 4 0 0 324 1 3 0 1 5 7 2]
 [ 4 3 4 19 1 330 3 0 0 1 3 9]
 [ 2 2 12 2 4 0 321 7 0 0 2 0]
 [ 3 7 1 1 2 0 4 344 1 0 0 0]
 [ 5 10 0 0 9 1 1 0 334 3 0 0]
 [ 4 2 1 0 33 0 2 2 7 317 4 1]
 [ 5 2 0 0 15 0 1 1 0 2 323 1]
 [ 4 17 0 33 2 8 0 1 0 2 3 302]]

从输出中可以观察到,尽管代码一开始的学习率为 0.001,但它在训练结束时将学习率降低到 0.001。由于有 12 个命令词,它还会输出一个 12 x 12 的混淆矩阵。

代码还会打印验证集的准确度和混淆矩阵,如下所示:

INFO:tensorflow:Step 18000: Validation accuracy = 88.5% (N=4445)
INFO:tensorflow:Saving to "/tmp/speech_commands_train/conv.ckpt-18000"
INFO:tensorflow:set_size=4890
INFO:tensorflow:Confusion Matrix:
 [[404 2 0 0 0 0 0 0 0 0 2 0]
 [ 1 283 10 3 14 15 15 22 12 4 10 19]
 [ 0 7 394 4 1 3 9 0 0 0 1 0]
 [ 0 8 7 353 0 7 9 1 0 0 0 20]
 [ 2 4 1 0 397 6 2 0 1 6 5 1]
 [ 1 8 1 36 2 342 6 1 0 0 0 9]
 [ 1 2 14 1 4 0 386 4 0 0 0 0]
 [ 1 9 0 2 1 0 10 368 3 0 1 1]
 [ 2 13 0 0 7 10 1 0 345 15 3 0]
 [ 1 8 0 0 34 0 3 1 14 329 7 5]
 [ 0 1 1 0 11 3 0 0 1 2 387 5]
 [ 3 16 2 58 6 9 3 2 0 1 1 301]]
  1. 最后,代码打印出测试集的准确度,如下所示:
INFO:tensorflow:Final test accuracy = 87.7% (N=4890)

就是这样。模型已经训练完成,可以通过 TensorFlow 导出并用于服务,或者嵌入到其他桌面、Web 或移动应用中。

总结

在本章中,我们学习了一个将音频数据转为文本的项目。现在有许多开源 SDK 和商业付费云服务,可以将音频记录和文件转换为文本数据。作为示例项目,我们使用了 Google 的语音命令数据集和 TensorFlow 基于深度学习的示例,将音频文件转换为语音命令进行识别。

在下一章,我们将继续这个旅程,构建一个使用高斯过程预测股票价格的项目,这是一个广泛用于预测的算法。

问题

  1. 混淆矩阵在训练结束时的解释是什么?

  2. 创建一个你和家人朋友录制的声音数据集。使用这个数据运行模型并观察准确率如何。

  3. 在你自己的数据集上重新训练模型,并检查你自己训练、验证和测试集的准确度。

  4. 尝试修改 train.py 中的不同选项,并在博客中分享你的发现。

  5. models.py 文件中添加不同的架构,看看你是否能为语音数据集或自己录制的数据集创建一个更好的架构。

进一步阅读

以下链接有助于深入了解语音转文本:

第六章:使用高斯过程回归预测股市价格

在本章中,我们将学习一种新的预测模型,叫做高斯过程,通常简称为GPs,它在预测应用中非常流行,尤其是在我们希望通过少量数据点建模非线性函数并量化预测的不确定性时。

我们将使用高斯过程来预测三只主要股票的股价,即谷歌、Netflix 和通用电气(GE)公司。

本章其余部分分为以下几节:

  • 理解贝叶斯定理

  • 贝叶斯推断

  • 引入高斯过程

  • 理解股市数据集

  • 应用高斯过程预测股市价格

理解贝叶斯定理

在开始我们的项目之前,我们先回顾一下贝叶斯定理及其相关术语。

贝叶斯定理用于描述一个事件的概率,基于与该事件可能相关的条件的先验知识。例如,假设我们想预测一个人是否患有糖尿病。如果我们知道初步的医学检查结果,我们就能比不知道检查结果时得到更准确的预测。让我们通过一些数字来从数学角度理解这个问题:

  • 1%的人口患有糖尿病(因此 99%的人没有)。

  • 初步测试在糖尿病存在时 80%的概率能检测出来(因此 20%的时间我们需要进行更高级的测试)

  • 初步检查在糖尿病不存在时有 10%的时间会误判为糖尿病(因此 90%的时间能给出正确结果):

糖尿病(1%) 无糖尿病(99%)
检测阳性 80% 10%
检测阴性 20% 90%

所以,如果一个人患有糖尿病,我们将查看第一列,他有 80%的机会被检测出糖尿病。而如果一个人没有糖尿病,我们将查看第二列,他有 10%的机会在初步测试中被误诊为糖尿病阳性。

假设一个人在初步检测中被诊断为糖尿病阳性。那么他实际上患糖尿病的几率有多大?

事实证明,著名科学家托马斯·贝叶斯(171-1761 年)提供了一个数学框架,用于在上述情况下计算概率。他的数学公式可以表示为:

其中:

  • 表示随机选中一个人患糖尿病的概率,这里是 1%。也被称为贝叶斯术语中的先验,它表示我们在没有任何额外信息时对一个事件的信念。

  • 表示在初步测试中,假设某人被检测出阳性结果后,该人患有糖尿病的概率。在贝叶斯术语中,这也称为后验概率,它表示在获得附加信息后,事件的更新概率。

  • 表示在初步测试中,假设某人患有糖尿病的情况下,获得阳性结果的概率。在此情况下为 80%。

  • 表示在初步测试中,随机一个人被检测为阳性的概率。这也可以写作:

贝叶斯法则在量化机器学习系统的预测不确定性中被广泛使用。

介绍贝叶斯推断

既然我们已经了解了贝叶斯法则的基础知识,接下来让我们尝试理解贝叶斯推断或建模的概念。

如我们所知,现实世界的环境始终是动态的、嘈杂的、观测代价高昂且时间敏感的。当商业决策基于这些环境中的预测时,我们不仅希望产生更好的预测结果,还希望量化这些预测结果中的不确定性。因此,贝叶斯推断理论在这种情况下非常有用,因为它提供了一个有原则的解决方法。

对于典型的时间序列模型,我们在给定x变量时,实际上是基于y进行曲线拟合。这有助于根据过去的观测数据拟合曲线。让我们尝试理解其局限性。考虑以下城市温度的例子:

日期 温度
5 月 1 日 10 AM 10.5 摄氏度
5 月 15 日 10 AM 17.5 摄氏度
5 月 30 日 10 AM 25 摄氏度

使用曲线拟合,我们得到以下模型:

然而,这意味着温度函数是线性的,在第十天时,我们预计温度为 15 摄氏度。常识告诉我们,城市的温度在一天中波动较大,而且它依赖于我们何时进行测量。曲线拟合定义了在给定的一组读数下的某一函数。

这个例子使我们得出结论,存在一组曲线可以建模给定的观测数据。建模这些观测数据的曲线分布的概念是贝叶斯推断或建模的核心。现在的问题是:我们应该如何从这一组函数中选择一个?或者,事实上,我们是否应该选择一个?

缩小函数族范围的一种方法是基于我们对问题的先验知识,筛选出其中的一个子集。例如,我们知道在五月份,我们不期望气温降到零摄氏度以下。我们可以利用这个知识,排除所有包含零度以下点的函数。另一种常见的思路是基于我们的先验知识在函数空间上定义一个分布。此外,在这种情况下,建模的任务就是根据观察到的数据点,细化可能函数的分布。由于这些模型没有定义的参数,因此它们通常被称为贝叶斯非参数模型

引入高斯过程

高斯过程(GP)可以被看作是一种替代的贝叶斯回归方法。它们也被称为无限维高斯分布。GP 定义了函数的先验分布,一旦我们观察到一些数据点,就可以将其转化为后验分布。尽管看起来无法对函数定义分布,但实际上我们只需要在观察到的数据点上定义函数值的分布。

形式上,假设我们在 n 个值 上观察到一个函数 ,并且在这些点上获得了 的值。该函数是一个高斯过程(GP),如果所有值 是联合高斯分布,其均值为 ,协方差为 ,由 给出。在这里, 函数定义了两个变量之间的关系。我们将在本节后面讨论不同类型的核函数。多个高斯变量的联合高斯分布也被称为多元高斯分布。

从前面的温度示例中,我们可以想象多种函数可以拟合给定的温度观测值。有些函数比其他函数更加平滑。捕捉平滑度的一种方法是使用协方差矩阵。协方差矩阵确保输入空间中相近的两个值()在输出空间中产生相近的值()。

本质上,我们试图通过 GP 解决的问题是:给定一组输入值 及其对应的值 ,我们试图估计新一组输入值 的输出值分布 。从数学角度看,我们试图估计的量可以表示为:

为了得到这个结果,我们将 建模为 GP,这样我们就知道 都来自一个具有以下均值和协方差函数的多元高斯分布:

其中, 分别表示 的先验均值,以及观察和未观察到的函数值上 的先验均值, 表示应用核函数于每个观察值后得到的矩阵,

核函数尝试将输入空间中两个数据点之间的相似性映射到输出空间。假设有两个数据点 ,其对应的函数值分别为 。核函数测量输入空间中两个点 之间的接近度是如何映射到它们的函数值 之间的相似性或相关性的。

我们将这个核函数应用于数据集中所有观测值对,从而创建一个被称为核/协方差矩阵 (K) 的相似度矩阵。假设有 10 个输入数据点,核函数将应用于每一对数据点,生成一个 10x10 的核矩阵 (K)。如果两个数据点 的函数值预计相似,那么在矩阵的 (i,j) 位置,核值预计会很高。在下一节中,我们将详细讨论 GP 中的不同核函数。

在这个方程中, 代表通过对训练集和测试集中的值应用相同的核函数得到的矩阵,而 是通过测量测试集中的输入值之间的相似性得到的矩阵。

此时,我们假设有一些线性代数魔法可以帮助我们从联合分布中获得条件分布 ,并得到以下结果:

我们将跳过推导过程,但如果你想了解更多,可以访问 Rasmussen 和 Williams 的资料(www.gaussianprocess.org/gpml/chapters/RW.pdf)。

有了这个解析结果,我们可以访问整个测试数据集上的函数值分布。将预测建模为分布也有助于量化预测的不确定性,这在许多时间序列应用中非常重要。

在高斯过程(GPs)中选择核函数

在许多应用中,我们发现先验均值通常设置为零,因为它简单、方便且在许多应用中效果良好。然而,为任务选择合适的核函数并不总是直观的。如前所述,核函数实际上试图将输入数据点之间的相似性映射到输出(函数)空间。核函数()的唯一要求是,它应该将任何两个输入值 映射到一个标量,使得核矩阵()是正定/半正定的,从而使其成为有效的协方差函数。

为了简洁起见,我们省略了协方差矩阵的基本概念及其如何始终是半正定矩阵的解释。我们鼓励读者参考MIT的讲义。

尽管对所有类型的核函数的完整讨论超出了本章的范围,但我们将讨论用于构建该项目的两种核函数:

  • 白噪声核:顾名思义,白噪声核将白噪声(方差)添加到现有的协方差矩阵中。从数学上讲,它可以表示为:

如果有很多设置,数据点不准确并且被一些随机噪声污染。输入数据中的噪声可以通过将白噪声核添加到协方差矩阵中来建模。

  • 平方指数(SE)核: 给定两个标量, ,平方指数核由以下公式给出:

这里,是一个缩放因子,而是平滑参数,它决定了核函数的平滑度。这个核函数非常流行,因为通过这个核得到的高斯过程函数是无限可微的,这使得它适用于许多应用。

这里是从具有 SE 核的高斯过程抽取的一些样本,固定为 1:

我们可以观察到,随着的增加,函数变得更加平滑。有关不同类型核函数的更多信息,请参阅The Kernel Cookbook (www.cs.toronto.edu/~duvenaud/cookbook/)

选择核函数的超参数

到目前为止,我们已经定义了具有不同参数的核函数。例如,在平方指数核中,我们有参数。我们将任何核函数的参数集表示为。现在的问题是,如何估计

如前所述,我们将函数的输出分布建模为从多元高斯分布中随机抽取的样本。这样,观察数据点的边际似然是一个条件化于输入点和参数的多元高斯分布。因此,我们可以通过最大化观察数据点在此假设下的似然来选择

现在我们已经理解了高斯过程是如何进行预测的,让我们看看如何利用高斯过程在股票市场进行预测,并可能赚取一些钱。

将高斯过程应用于股票市场预测

在这个项目中,我们将尝试预测市场中三只主要股票的价格。该练习的数据集可以从 Yahoo Finance 下载(finance.yahoo.com)。我们下载了三家公司完整的股票历史数据:

我们选择了三个数据集来比较不同股票的高斯过程性能。欢迎尝试更多的股票。

所有这些数据集都存在于 GitHub 仓库中。因此,运行代码时无需再次下载它们。

数据集中的 CSV 文件包含多个列,内容如下:

  • Date: 股票价格测量的日历日期。

  • Open: 当天的开盘价。

  • High: 当天的最高价。

  • Low: 当天的最低价。

  • Close: 当天的收盘价。

  • Adj Close: 调整后的收盘价是股票的收盘价,在下一个交易日开盘前,已被修正以包含任何股息或其他公司行动。这是我们的目标变量或数据集中的 Y。

  • Volume: 交易量表示当天交易的股票数量。

为了开始我们的项目,我们将考虑每个股票数据集的两个预测问题:

  • 在第一个问题中,我们将使用 2008-2016 年的价格进行训练,并预测 2017 年的所有价格。

  • 在第二个问题中,我们将使用 2008-2018 年(至第三季度)的价格进行训练,并预测 2018 年第四季度的价格。

对于股票价格预测,我们不需要像许多经典方法(例如回归)那样将股票的整个时间序列建模为一个单一的时间序列。对于高斯过程(GP),每只股票的时间序列会被分割成多个时间序列(每年一个时间序列)。直观地讲,这是合理的,因为每只股票都遵循一个年度周期。

每年股票的时间序列作为输入,作为独立的时间序列输入到模型中。因此,预测问题变成了:给定多个年度时间序列(每个历史年份一个),预测股票的未来价格。由于高斯过程模型是函数的分布,我们希望预测未来每个数据点的均值和不确定性。

在建模之前,我们需要将价格标准化为零均值和单位标准差。这是高斯过程的要求,原因如下:

  • 我们假设输出分布的先验为零均值,因此需要进行标准化以匹配我们的假设。

  • 许多协方差矩阵的核函数中有尺度参数。标准化输入有助于我们更好地估计核函数参数。

  • 要获得高斯过程中的后验分布,我们必须反转协方差矩阵。标准化有助于避免此过程中出现任何数值问题。请注意,在本章中我们没有详细讨论获取后验的线性代数。

一旦数据被标准化,我们就可以训练我们的模型并使用高斯过程预测价格。对于建模,我们使用来自 GPflow 库的插件和即插即用功能(github.com/GPflow/GPflow),它是一个基于 TensorFlow 的高斯过程封装库。

预测问题中的自变量(X)由两个因素组成:

  • 每年

  • 每年的日期

问题中的因变量(Y)是每年每天的标准化调整后收盘价,如前所述。

在训练模型之前,我们需要为高斯过程定义先验和核函数。对于这个问题,我们使用标准的零均值先验。我们使用一个核函数来生成协方差矩阵,该矩阵是两个核函数的和,定义如下:

  • 平方指数(或 RBF,如在 GPflow 包中所提到)核函数,lengthscale = 1variance = 63。

  • 白噪声的初始variance非常低,如1e-10

选择平方指数核函数的思路是它是无限可微的,并且是最容易理解的。白噪声用于考虑我们在目标变量中可能观察到的任何系统性噪声。虽然它可能不是最好的核函数选择,但它有助于理解。您可以自由尝试其他核函数,看看它们是否效果良好。

创建股票价格预测模型

我们将通过处理数据集中的数据开始我们的项目:

  1. 创建一个数据框,其中包含每只股票的年度时间序列。每年的股票价格在数据框中由单独的列表示。将数据框中的行数限制为 252 行,这大约是每年的交易日数。还要为每行数据添加与之相关的财务季度,作为一个单独的列。
def get_prices_by_year(self):
   df = self.modify_first_year_data()
   for i in range(1, len(self.num_years)):
       df = pd.concat([df, pd.DataFrame(self.get_year_data(year=self.num_years[i], normalized=True))], axis=1)
   df = df[:self.num_days]
   quarter_col = []
   num_days_in_quarter = self.num_days // 4
   for j in range(0, len(self.quarter_names)):
       quarter_col.extend([self.quarter_names[j]]*num_days_in_quarter)
   quarter_col = pd.DataFrame(quarter_col)
   df = pd.concat([df, quarter_col], axis=1)
   df.columns = self.num_years + ['Quarter']
   df.index.name = 'Day'
   df = self.fill_nans_with_mean(df)
   return df

请注意,一年中大约有 252 个交易日,因为股市在周末休市。

  1. 即使某一年的交易日数更多(例如闰年),也将数据限制为 252 天,以确保各年份之间的一致性。如果某一年份的交易日数少于 252 天,则通过对缺失的天数进行填补(使用该年的均值价格)来外推数据以达到 252 天。请使用以下代码来实现此功能:
def fill_nans_with_mean(self, df):
   years = self.num_years[:-1]
   df_wo_last_year = df.loc[:,years]
   df_wo_last_year = df_wo_last_year.fillna(df_wo_last_year.mean())
   df_wo_last_year[self.num_years[-1]] = df[self.num_years[-1]]
   df= df_wo_last_year

   return df
  1. 对每一年进行价格归一化,将年度数据转化为零均值和单位标准差。同时,从该年所有数据点中减去第一天的价格。这基本上强制每年的时间序列从零开始,从而避免了前一年价格对其的影响。
def normalized_data_col(self, df):
   price_normalized = pd.DataFrame()
   date_list = list(df.Date)
   self.num_years = sorted(list(set([date_list[i].year for i in range(0, len(date_list))])))
   for i in range(0, len(self.num_years)):
       prices_data = self.get_year_data(year=self.num_years[i], normalized=False)
       prices_data = [(prices_data[i] - np.mean(prices_data)) / np.std(prices_data) for i in range(0, len(prices_data))]
       prices_data = [(prices_data[i] - prices_data[0]) for i in range(0, len(prices_data))]
       price_normalized = price_normalized.append(prices_data, ignore_index=True)
   return price_normalized

在执行代码之前,请确保按照README文件中的说明安装本章的相关库。

  1. 如前节所述,生成协方差矩阵作为两个核函数的和:
kernel = gpflow.kernels.RBF(2, lengthscales=1, variance=63) + gpflow.kernels.White(2, variance=1e-10)

我们使用 GPflow 包中的 SciPy 优化器,通过最大似然估计来优化超参数。SciPy 是 Python 库中的标准优化器。如果您不熟悉 SciPy 优化器,请参考官方页面(docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html)。

  1. 实现最终的包装函数make_gp_predictions来训练高斯过程模型并进行未来价格预测。该函数实现的步骤如下:

    1. 输入训练数据、训练周期的开始和结束日期以及预测年份和季度。

    2. 使用训练周期的起始年份数据构建 2 个独立的系列,一个用于自变量(X),一个用于目标变量(Y)。系列(X)中的每个元素代表年份中的每一天,并由两个自变量组成:年份和该年的第几天。例如,对于起始年份 2008 年,X 的形式为[[2008,1], [2008,2], [2008,3], ...... [2008,252]]。

    3. 将每个后续年份的自变量和目标变量分别附加到列表 X 和 Y 中。

    4. 如果输入pred_quarters不为 None,则根据指定的季度进行预测,而不是整个年份。例如,如果pred_quarters为[4]且pred_year为 2018 年,则该函数会使用 2018 年第三季度之前的所有数据来预测 2018 年第四季度的股价。

    5. 定义如前所述的核函数,并使用 Scipy 优化器训练 GP 模型。

    6. 对预测期内的股票价格进行预测。

def make_gp_predictions(self, start_year, end_year, pred_year, pred_quarters = []):
   start_year, end_year, pred_year= int(start_year),int(end_year), int(pred_year)
   years_quarters = list(range(start_year, end_year + 1)) + ['Quarter']
   years_in_train = years_quarters[:-2]
   price_df = self.preprocessed_data.prices_by_year[self.preprocessed_data.prices_by_year.columns.intersection(years_quarters)]
   num_days_in_train = list(price_df.index.values)
   #Generating X and Y for Training
   first_year_prices = price_df[start_year]
   if start_year == self.preprocessed_data.num_years[0]:
       first_year_prices = (first_year_prices[first_year_prices.iloc[:] != 0])
       first_year_prices = (pd.Series([0.0], index=[first_year_prices.index[0]-1])).append(first_year_prices)
   first_year_days = list(first_year_prices.index.values)
   first_year_X = np.array([[start_year, day] for day in first_year_days])
   X = first_year_X
   Target = np.array(first_year_prices)
   for year in years_in_train[1:]:
       current_year_prices = list(price_df.loc[:, year])
       current_year_X = np.array([[year, day] for day in num_days_in_train])
       X = np.append(X, current_year_X, axis=0)
       Target = np.append(Target, current_year_prices)
   final_year_prices = price_df[end_year]
   final_year_prices = final_year_prices[final_year_prices.iloc[:].notnull()]
   final_year_days = list(final_year_prices.index.values)
   if pred_quarters is not None:
       length = 63 * (pred_quarters[0] - 1)
       final_year_days = final_year_days[:length]
       final_year_prices = final_year_prices[:length]
   final_year_X = np.array([[end_year, day] for day in final_year_days])
   X = np.append(X, final_year_X, axis=0)
   Target = np.append(Target, final_year_prices)
   if pred_quarters is not None:
       days_for_prediction = [day for day in
                              range(63 * (pred_quarters[0]-1), 63 * pred_quarters[int(len(pred_quarters) != 1)])]
   else:
       days_for_prediction = list(range(0, self.preprocessed_data.num_days))
   x_mesh = np.linspace(days_for_prediction[0], days_for_prediction[-1]
                        , 2000)
   x_pred = ([[pred_year, x_mesh[i]] for i in range(len(x_mesh))])
   X = X.astype(np.float64)
   Target = np.expand_dims(Target, axis=1)
   kernel = gpflow.kernels.RBF(2, lengthscales=1, variance=63) + gpflow.kernels.White(2, variance=1e-10)
   self.gp_model = gpflow.models.GPR(X, Target, kern=kernel)
   gpflow.train.ScipyOptimizer().minimize(self.gp_model)
   y_mean, y_var = self.gp_model.predict_y(x_pred)
   return x_mesh, y_mean, y_var

理解所得结果

让我们尝试了解我们对每只股票的预测效果如何:

  • Netflix (NFLX):以下图表展示了2002年到2018年期间 Netflix 股票的价格:

2018年的价格通过两条垂直线定义,显示了整个年度股票价格的增长情况。

根据第一个问题案例,我们考虑2008-2016年间的数据进行训练:

对每年的价格进行标准化建模,得到以下图表:

2017年整个年度股票价格进行预测,并给出95%置信区间的结果,得到以下图表:

将生成的值与实际值进行比较,可以看出该模型预测的值低于实际值。然而,造成这种情况的原因可能是2016年 Netflix 股票价格的波动。这些波动没有通过该项目中使用的基础核函数捕捉到。

对于第二个问题案例,我们考虑2008-2018年的训练周期,包括前三个季度。此期间 Netflix 股票价格的图表如下:

使用标准化值,我们得到以下图表:

正如我们所见,这一预测很好地捕捉了趋势,同时也体现了不确定性。

  • 通用电气公司GE):为了理解实际价格与预测价格之间的差异,有必要绘制包含实际值的图表。以下是展示 GE 股票历史价格的图表:

如前所述,虚线垂直线表示2018年的股价。

根据我们的第一个问题案例,我们考虑了2008-2016年期间的训练数据。该期间的图表如下:

2009年出现了一个巨大的跌幅,但从那时起,股票价格一直在稳步增长。

由于我们在建模时使用了每年标准化价格,让我们来看一下模型的输入数据:

对于2017年的标准化价格预测,95%置信区间的结果如下:

如我们所见,模型准确地捕捉到了股票的趋势。

对于第二次预测,我们考虑了2008-2018年期间的训练数据,包括2018年前三个季度。在此期间,GE 股票的价格图如下:

该期间的预测价格,95%置信区间为:

  • GoogleGOOG):以下图表展示了谷歌股票的历史价格:

如前所述,虚线垂直线代表的是2018年的价格。

根据第一个问题案例,2008-2016年期间对于训练至关重要,让我们来看一下这个期间的图表:

2009年出现了巨大的跌幅,之后股票价格稳步上涨,除非在2015年。

由于我们在建模时使用了按年标准化的价格,让我们来看一下模型的输入数据:

对于整年2017的预测(标准化价格),95%置信区间的结果如下:

我们能够捕捉到价格的整体上升趋势,但置信区间非常宽。

对于下一个预测,我们考虑了2008-2018年的训练期,包括前三个季度。在此期间的谷歌股票价格图如下:

该期间的预测价格,95%置信区间为:

2018 年,整体趋势得到了更好的捕捉。

总结

在这一章中,我们了解了一种非常流行的贝叶斯预测模型——高斯过程,并使用它来预测股票价格。

在本章的第一部分,我们通过从多变量高斯分布中采样一个合适的函数来研究预测问题,而不是使用单点预测。我们研究了一种特殊的非参数贝叶斯模型,称为高斯过程。

随后,我们使用高斯过程(GP)预测了三只股票——即谷歌、奈飞和 GE——在 2017 年和 2018 年第四季度的价格。我们观察到,预测结果大多数落在 95%的置信区间内,但仍远非完美。

高斯过程广泛应用于需要在数据点非常少的情况下建模带有不确定性的非线性函数的场景。然而,在面对维度极高的问题时,它们有时会失效,而其他深度学习算法,如 LSTM,可能表现得更好。

在下一章,我们将深入探讨一种无监督的方法,通过自动编码器检测信用卡欺诈行为。

问题

  1. 什么是高斯过程?

  2. 你能通过尝试不同的核函数来改善预测结果吗?

  3. 你能将高斯过程模型应用于标准普尔 500 指数中的其他股票,并与这里提到的股票表现进行比较吗?

第七章:使用自编码器进行信用卡欺诈检测

数字世界正在快速发展。我们习惯于在网上完成许多日常任务,如预订出租车、在电子商务网站购物,甚至为手机充值。对于这些任务的大多数,我们习惯使用信用卡支付。然而,信用卡可能会被盗用,这可能导致欺诈交易。Nilson 报告估计,每花费 100 美元,就有 7 美分被盗取。它估计信用卡欺诈市场总额约为 300 亿美元。

检测交易是否欺诈是一个非常具有影响力的数据科学问题。每家发行信用卡的银行都在投资技术以检测欺诈并立即采取适当的行动。有很多标准的监督学习技术,如逻辑回归、随机森林等,用于欺诈分类。

在本章中,我们将更详细地了解使用自编码器检测信用卡欺诈的无监督方法,并探讨以下主题:

  • 了解自编码器

  • 定义和训练欺诈检测模型

  • 测试欺诈检测模型

了解自编码器

自编码器是一种人工神经网络,其任务是使用无监督学习学习输入数据的低维表示。当涉及到输入数据的降维和生成模型时,它们非常受欢迎。

本质上,自编码器学习将数据压缩成低维表示,然后将该表示重建为与原始数据匹配的内容。这样,低维表示忽略了噪声,这些噪声在重建原始数据时没有帮助。

如前所述,它们在生成模型,特别是图像方面也很有用。例如,如果我们输入飞行的表示,它可能会尝试生成一个飞行的猫图像,尽管它以前未见过这样的图像。

从结构上看,自编码器由两个部分组成,编码器和解码器。编码器生成输入的低维表示,解码器帮助从表示中重新生成输入。通常,编码器和解码器是具有一个或多个隐藏层的前馈神经网络。

以下图示说明了典型自编码器的配置:

要定义编码器()和解码器(),自编码器可以通过以下数学公式表示:

如公式所示,编码器和解码器的参数通过一种优化方式进行调整,以最小化一种特殊类型的错误,称为重建误差。重建误差是重建输入与原始输入之间的误差。

构建欺诈检测模型

对于这个项目,我们将使用 Kaggle 上的信用卡数据集(www.kaggle.com/mlg-ulb/creditcardfraud),Andrea Dal Pozzolo,Olivier Caelen,Reid A. Johnson 和 Gianluca Bontempi 的论文《Calibrating Probability with Undersampling for Unbalanced Classification》,发表于 2015 年计算智能与数据挖掘研讨会(CIDM),IEEE。该数据集包含来自欧洲持卡人的两天信用卡交易数据,数据严重不平衡,共约 28.4 万条交易数据,其中有 492 个欺诈实例(占总数的 0.172%)。

数据集中有 31 个数值型列,其中两个是时间和金额。Time表示从每笔交易到数据集中第一次交易所经过的时间(单位:秒)。Amount表示交易的总金额。对于我们的模型,我们将去除时间列,因为它对模型的准确性没有帮助。其余的特征(V1, V2 ... V28)是通过主成分分析(ocw.mit.edu/courses/mathematics/18-650-statistics-for-applications-fall-2016/lecture-videos/lecture-19-video/)从原始特征中提取的,出于保密原因。Class是目标变量,表示交易是否为欺诈交易。

数据预处理的工作并不多,主要是因为大部分数据已经清理过。

通常,在经典的机器学习模型中,如逻辑回归,我们会将负类和正类的数据点都输入到算法中。然而,由于我们使用的是自动编码器,我们将采用不同的建模方式。

本质上,我们的训练集将仅包含非欺诈交易数据。我们的思路是,每当我们将一笔欺诈交易输入到已训练的模型中时,模型应将其检测为异常。我们将这个问题定义为异常检测问题,而不是分类问题。

该模型将由两个全连接的编码器层组成,分别有 14 个和 7 个神经元。解码器部分将有两个层,分别有 7 个和 29 个神经元。此外,我们将在训练过程中使用 L1 正则化。

最后,为了定义模型,我们将使用 Keras,并以 Tensorflow 作为后端来训练自动编码器。

正则化是机器学习中的一种技术,用于减少过拟合。过拟合发生在模型不仅学习到训练数据中的信号,还学习到了噪音,从而无法很好地泛化到未见过的数据集。虽然有很多方法可以避免过拟合,比如交叉验证、采样等,但正则化特别是通过对模型的权重添加惩罚,防止我们学习到过于复杂的模型。L1 正则化对模型的所有权重添加 L1 范数惩罚。这样,任何不贡献于准确度的权重都会被压缩到零。

定义并训练欺诈检测模型

以下是定义和训练模型的步骤:

  1. 通过去除均值并将其缩放到单位方差,转换'Amount'
def preprocess_data(data):
data = data.drop(['Time'], axis=1)
data['Amount'] = StandardScaler().fit_transform(data['Amount'].values.reshape(-1, 1))
return
 data

请注意,我们使用的是来自 scikit-learn 的StandardScaler工具来实现此目的。

  1. 为了构建我们的数据集,将其分为训练数据和测试数据,训练数据仅包含非欺诈交易,测试数据包含欺诈和非欺诈交易:
def get_train_and_test_data(processed_data):
X_train, X_test = train_test_split(processed_data, test_size=0.25, random_state=RANDOM_SEED)
X_train = X_train[X_train.Class == 0]
X_train = X_train.drop(['Class'], axis=1)
y_test = X_test['Class']
X_test = X_test.drop(['Class'], axis=1)
X_train = X_train.values
X_test = X_test.values
return X_train, X_test,y_test
  1. 使用以下代码定义模型:
def define_model(self):
dim_input = self.train_data.shape[1]
layer_input = Input(shape=(dim_input,))
layer_encoder = Dense(DIM_ENCODER, activation="tanh",
activity_regularizer=regularizers.l1(10e-5))(layer_input)
layer_encoder = Dense(int(DIM_ENCODER / 2), activation="relu")(layer_encoder)
layer_decoder = Dense(int(DIM_ENCODER / 2), activation='tanh')(layer_encoder)
layer_decoder = Dense(dim_input, activation='relu')(layer_decoder)
autoencoder = Model(inputs=layer_input, outputs=layer_decoder)
return autoencoder
  1. 一旦模型定义完成,使用 Keras 训练模型:
def train_model(self):
self.model.compile(optimizer=OPTIMIZER,
loss=LOSS,
metrics=[EVAL_METRIC])
checkpoint = ModelCheckpoint(filepath=os.path.join(MODEL_SAVE_DIR, "trained_model.h5"),
verbose=0,
save_best_only=True)
log_tensorboard = TensorBoard(log_dir='./logs',
histogram_freq=0,
write_graph=True,
write_images=True)
history = self.model.fit(self.train_data, self.train_data,
epochs=EPOCHS,
batch_size=BATCH_SIZE,
shuffle=True,
validation_data=(self.test_data, self.test_data),
verbose=1,
callbacks=[checkpoint, log_tensorboard]).history
self.history = history
print("Training Done. Plotting Loss Curves")
self.plot_loss_curves()
  1. 使用以下参数查找模型的输出:
    • EPOCHS = 100。

    • BATCH_SIZE = 32。

    • OPTIMIZER = 'Adam'。

    • LOSS = 重建输入与原始输入之间的均方误差。

    • EVAL_METRIC = 'Accuracy'。这是通常的二分类准确率。

  1. 存储一个TensorBoard文件,以可视化图形或其他变量。同时,通过 Keras 提供的检查点存储表现最佳的模型。生成训练和测试数据的损失曲线:
def plot_loss_curves(self):
fig = plt.figure(num="Loss Curves")
fig.set_size_inches(12, 6)
plt.plot(self.history['loss'])
plt.plot(self.history['val_loss'])
plt.title('Loss By Epoch')
plt.ylabel('Loss')
plt.xlabel('Epoch Num')
plt.legend(['Train_Data', 'Test_Data'], loc='upper right');
plt.grid(True, alpha=.25)
plt.tight_layout()
image_name = 'Loss_Curves.png'
fig.savefig(os.path.join(PLOTS_DIR,image_name), dpi=fig.dpi)
plt.clf()
  1. 以下图示展示了在训练 100 个 epochs 时生成的损失曲线:

我们可以观察到,对于训练集,损失或重建误差在训练初期下降,并在结束时饱和。此饱和意味着模型已经完成了权重的学习。

保存测试集中损失最小的模型。

测试欺诈检测模型

一旦训练过程完成,通过欺诈交易和非欺诈(正常)交易区分测试集中的重建误差。生成不同类别交易的重建误差:

def plot_reconstruction_error_by_class(self):
self.get_test_predictions()
mse = np.mean(np.power(self.test_data - self.test_predictions, 2), axis=1)
self.recon_error = pd.DataFrame({'recon_error': mse,
'true_class': self.y_test})
## Plotting the errors by class
# Normal Transactions
fig = plt.figure(num = "Recon Error with Normal Transactions")
fig.set_size_inches(12, 6)
ax = fig.add_subplot(111)
normal_error_df = self.recon_error[(self.recon_error['true_class'] == 0) & (self.recon_error['recon_error'] < 50)]
_ = ax.hist(normal_error_df.recon_error.values, bins=20)
plt.xlabel("Recon Error Bins")
plt.ylabel("Num Samples")
plt.title("Recon Error with Normal Transactions")
plt.tight_layout()
image_name = "Recon_Error_with_Normal_Transactions.png"
fig.savefig(os.path.join(PLOTS_DIR, image_name), dpi=fig.dpi)
plt.clf()

以下图示展示了测试集中欺诈和正常交易的重建误差分布:

下一个图示为欺诈交易:

如我们所见,大多数正常交易的重建误差接近零。然而,欺诈交易的重建误差分布较广,大部分仍接近零。

这表明重建误差的阈值可以作为正常交易与欺诈交易的分类阈值。

为了评估模型,我们将使用在第四章《使用 TensorFlow Lite 进行数字分类》中定义的精确度和召回率度量。首先,让我们来看一下在不同重构误差阈值下的精确度和召回率:

def get_precision_recall_curves(self):
precision, recall, threshold = precision_recall_curve(self.recon_error.true_class, self.recon_error.recon_error)
# Plotting the precision curve
fig = plt.figure(num ="Precision Curve")
fig.set_size_inches(12, 6)
plt.plot(threshold, precision[1:], 'g', label='Precision curve')
plt.title('Precision By Recon Error Threshold Values')
plt.xlabel('Threshold')
plt.ylabel('Precision')
plt.tight_layout()
image_name = 'Precision_Threshold_Curve.png'
fig.savefig(os.path.join(PLOTS_DIR, image_name), dpi=fig.dpi)
plt.clf()
plt.plot(threshold, recall[1:], 'g', label='Recall curve')
plt.title('Recall By Recon Error Threshold Values')
plt.xlabel('Threshold')
plt.ylabel('Recall')
plt.tight_layout()
image_name = 'Recall_Threshold_Curve.png'
fig.savefig(os.path.join(PLOTS_DIR, image_name), dpi=fig.dpi)
plt.clf()

精确度和召回率的重构误差阈值如以下图所示:

该图表示召回率的误差阈值:

如我们所见,当重构误差增加时,召回率下降,精确度则相反。由于数据集的原因,图中存在一些波动。

还有一件事我们需要记住。正如前面所提到的,在机器学习中,高精度与高召回率之间总是存在权衡。我们需要为我们的特定模型选择其中一个。

通常,企业更倾向于选择高精度或高召回率的模型。对于欺诈检测,我们希望有一个高召回率的模型。这一点非常重要,因为我们可以将大多数欺诈交易分类为欺诈。应对精确度损失的一种方法是对被分类为欺诈的交易进行人工验证,确定它们是否真的属于欺诈交易。这样有助于确保最终用户的良好体验。

这是生成混淆矩阵的代码,min_recall设置为 80%:

def get_confusion_matrix(self, min_recall = 0.8):
# Get the confusion matrix with min desired recall on the testing dataset used.
precision, recall, threshold = precision_recall_curve(self.recon_error.true_class, self.recon_error.recon_error)
idx = filter(lambda x: x[1] > min_recall, enumerate(recall[1:]))[-1][0]
th = threshold[idx]
print ("Min recall is : %f, Threshold for recon error is: %f " %(recall[idx+1], th))
# Get the confusion matrix
predicted_class = [1 if e > th else 0 for e in self.recon_error.recon_error.values]
cnf_matrix = confusion_matrix(self.recon_error.true_class, predicted_class)
classes = ['Normal','Fraud']
fig = plt.figure(figsize=(12, 12))
plt.imshow(cnf_matrix, interpolation='nearest', cmap=plt.cm.Blues)
plt.title("Confusion Matrix")
plt.colorbar()
tick_marks = np.arange(len(classes))
plt.xticks(tick_marks, classes, rotation=45)
plt.yticks(tick_marks, classes)
fmt = 'd'
thresh = cnf_matrix.max() / 2.
for i, j in itertools.product(range(cnf_matrix.shape[0]), range(cnf_matrix.shape[1])):
plt.text(j, i, format(cnf_matrix[i, j], fmt),
horizontalalignment="center",
color="white" if cnf_matrix[i, j] > thresh else "black")
plt.ylabel('True label')
plt.xlabel('Predicted label')
plt.tight_layout()
image_name = 'Confusion_Matrix_with_threshold_{}.png'.format(th)
fig.savefig(os.path.join(PLOTS_DIR, image_name), dpi=fig.dpi)
plt.clf()

从前面的代码得到的混淆矩阵如下图所示:

我们可以观察到,在 120 笔欺诈交易中,97 笔被正确分类。然而,我们还错误地将 1,082 笔正常交易分类为欺诈交易,这些交易将需要经过人工验证,以确保最终用户的良好体验。

作为一个警告,我们不应假设自编码器在所有二分类任务中都能有所帮助,并且能够超越最先进的分类模型。这个项目的目的是展示一种不同的使用自编码器进行分类任务的方法。

请注意,在本章中,为了说明问题,我们使用了相同的验证集和测试集。理想情况下,一旦我们定义了重构误差的阈值,应该在一些未见过的数据集上测试模型,以便更好地评估其性能。

总结

信用卡欺诈在本质上是普遍存在的。如今,每家公司都在使用机器学习来打击平台上的支付欺诈。在本章中,我们探讨了如何使用 Kaggle 的信用卡数据集来进行欺诈分类。

我们学习了自编码器作为一种降维技术。我们理解到,自编码器架构由两个组件组成:编码器和解码器。我们使用重构损失来建模一个全连接网络的参数。

此后,我们通过异常检测问题的视角来审视欺诈分类问题。我们使用正常交易训练了自动编码器模型。然后,我们查看了自动编码器在正常交易和欺诈交易中的重构误差,并观察到欺诈交易的重构误差分布较广。接着,我们在重构误差上定义了一个阈值来对模型进行分类,并生成了混淆矩阵。

在下一章中,我们将探讨贝叶斯神经网络的概念,它将深度学习和贝叶斯学习的概念结合起来,以在深度神经网络的预测中建模不确定性。

问题

以下是问题:

  • 什么是自动编码器?

  • 自动编码器的不同组件是什么?

  • 什么是重构损失?

  • 什么是精确度和召回率?

第八章:使用贝叶斯神经网络生成交通标志分类器的不确定性

作为人类,我们喜欢预测中的不确定性。例如,在我们离开家之前,我们总是想知道下雨的几率。然而,传统的深度学习只给出一个点预测,而没有不确定性的概念。这些网络的预测被假设为准确的,但事实并非总是如此。理想情况下,我们希望在做出决策之前,能够了解神经网络预测的置信度。

例如,在模型中引入不确定性本可能避免以下灾难性后果:

从这些例子中可以看出,量化预测中的不确定性本可以避免这些灾难。现在的问题是:如果这这么显而易见,为什么特斯拉或谷歌一开始没有实施呢?

贝叶斯算法(如高斯过程)能够量化不确定性,但无法扩展到像图像和视频这样的庞大数据集,而深度学习能够提供更好的准确性——只是它缺乏不确定性的概念。

本章将探索贝叶斯神经网络的概念,结合深度学习和贝叶斯学习的概念,以及深度神经网络预测中的模型不确定性。我们将涵盖以下主题:

  • 贝叶斯深度学习简介

  • 贝叶斯神经网络是什么?

  • 使用德国交通标志图像数据集构建贝叶斯神经网络

理解贝叶斯深度学习

我们都已经理解了贝叶斯规则的基础知识,如第六章所解释的,使用高斯过程回归预测股价

对于贝叶斯机器学习,我们使用与贝叶斯规则相同的公式来从给定数据中学习模型参数(),。因此,公式如下所示:

在这里, 或者观测数据的概率也叫做证据。这通常很难计算。一种暴力方法是将 对所有模型参数值进行积分,但显然这在计算上代价太高。 是参数的先验,在大多数情况下它只是参数的随机初始化值。通常,我们不在乎先验设置得是否完美,因为我们期望推断过程能够收敛到正确的参数值。

被称为给定建模参数下的数据似然性。实际上,它显示了在给定模型参数时,获得数据中给定观察值的可能性。我们使用似然性作为评估不同模型的标准。似然性越高,模型越好。

最终,,后验,是我们想要计算的内容。它是基于给定数据得到的模型参数的概率分布。一旦我们获得了模型参数的不确定性,我们就可以用它们来量化模型预测的不确定性。

通常,在机器学习中,我们使用最大似然估计MLE)(ocw.mit.edu/courses/mathematics/18-05-introduction-to-probability-and-statistics-spring-2014/readings/MIT18_05S14_Reading10b.pdf) 来获得模型参数的估计。然而,在贝叶斯深度学习的情况下,我们从先验和该过程估计后验,这一过程被称为最大后验估计MAP)(ocw.mit.edu/courses/sloan-school-of-management/15-097-prediction-machine-learning-and-statistics-spring-2012/lecture-notes/MIT15_097S12_lec15.pdf)。

神经网络中的贝叶斯法则

传统上,神经网络通过优化权重和偏差来产生一个点估计,从而最小化损失函数,例如回归问题中的均方误差。正如前面所提到的,这类似于使用最大似然估计准则来寻找参数:

通常,我们通过神经网络中的反向传播获得最佳参数。为了避免过拟合,我们引入了对权重的正则化,即!范数。如果你不熟悉正则化,请参考以下 Andrew Ng 的视频:openclassroom.stanford.edu/MainFolder/CoursePage.php?course=MachineLearning。研究表明,!归一化相当于对权重!施加了一个正态先验。在权重上有先验的情况下,MLE 估计问题可以被框架化为 MAP 估计:

使用贝叶斯定理,前面的方程可以写成如下形式:

正则化与贝叶斯框架等价的精确证明超出了本章的范围。如果你感兴趣,可以阅读以下 MIT 讲座,了解更多信息:www.mit.edu/~9.520/spring09/Classes/class15-bayes.pdf

从中我们可以观察到,带正则化的传统神经网络可以被框架化为使用贝叶斯定理的推理问题。贝叶斯神经网络的目标是使用蒙特卡洛方法或变分推理技术来确定后验分布。在本章的其余部分,我们将看看如何使用 TensorFlow Probability 构建贝叶斯神经网络。

理解 TensorFlow 概率、变分推理和蒙特卡洛方法

TensorFlow Probability(在代码中为tfpwww.tensorflow.org/probability/overview#layer_2_model_building)是谷歌最近发布的一个工具,用于以可扩展的方式执行概率推理。它提供了定义分布、构建带权重先验的神经网络以及执行概率推理任务(如蒙特卡洛方法或变分推理)所需的工具和功能。

让我们看一下在构建模型时将会使用的一些函数/工具:

  • Tfp.distributions.categorical:这是一个标准的类别分布,其特点是对 K 个类别的概率或对数概率。在这个项目中,我们有来自 43 种不同交通标志的交通标志图像。我们将在本项目中定义一个覆盖 43 个类别的类别分布。

  • 概率层:构建在 TensorFlow 层实现之上,概率层将它们所表示的函数中的不确定性融入其中。实际上,它们将不确定性融入神经网络的权重中。它们具有通过从权重分布的后验进行采样,前向传播输入的功能()。具体来说,我们将使用 Convolutional2DFlipout 层(www.tensorflow.org/probability/api_docs/python/tfp/layers/Convolution2DFlipout),它能够通过从模型权重参数的后验进行采样,计算前向传播。

  • Kullback-Leibler (KL) 散度:如果我们想要衡量两个数字之间的差异,只需将它们相减。那么,如果我们想要衡量两个概率分布之间的差异呢?在这种情况下,相减的等价物是什么?在概率和统计的情况下,我们通常会用一个更简单的近似分布来替代观察数据或复杂的分布。KL 散度帮助我们衡量在选择近似时丢失了多少信息。从本质上讲,它是衡量一个概率分布与其他分布之间差异的度量。KL 散度为 0 表示两个分布是相同的。如果你想了解更多关于 KL 散度的数学原理,请参考 MIT 开放课程中的精彩解释,链接为 ocw.mit.edu/courses/sloan-school-of-management/15-097-prediction-machine-learning-and-statistics-spring-2012/lecture-notes/MIT15_097S12_lec15.pdf

  • 变分推断:变分推断是一种机器学习方法,用于通过优化在贝叶斯学习中近似复杂、难以求解的积分。

正如我们所知,我们在贝叶斯学习中的目标是计算后验概率!,给定先验! 和数据!。计算后验的前提是计算!(数据)的分布,以便获得!,或称为 证据。如前所述,X 的分布是难以处理的,因为使用暴力计算方法计算其分布代价过高。为了解决这个问题,我们将使用一种叫做变分推断(VI)的方法。在 VI 中,我们定义一个由! 参数化的分布族!。核心思想是优化!,使得近似分布尽可能接近真实的后验分布。我们通过 KL 散度来衡量两个分布之间的差异。事实证明,最小化 KL 散度并不容易。我们可以通过以下数学公式向你展示,这个 KL 散度总是为正,并且它包含两个部分:

在这里,ELBO证据下界www.cs.princeton.edu/courses/archive/fall11/cos597C/lectures/variational-inference-i.pdf)。

  • 蒙特卡洛方法:蒙特卡洛方法是一种计算方法,通过反复随机抽样来获取某些现象(行为)的统计特性。它们通常用于模拟不确定性或生成商业场景的假设情况。

假设你每天都乘火车上下班。你在考虑是否改乘公司的班车去办公室。现在,公交车的乘坐过程涉及许多随机变量,如到达时间、交通、上车的乘客数量等等。

我们可以通过计算这些随机变量的平均值并计算到达时间来分析这种假设场景。然而,这种方法太过简单,因为它没有考虑到这些变量的方差。另一种方法是从这些随机变量中进行采样(如果你能够做到的话!),并生成假设场景以预测到达办公室的情况。

为了做出决策,你需要一个可接受的标准。例如,如果你观察到在 80% 的假设场景中你能准时到达办公室,那么你可以继续前进。这种方法也称为蒙特卡洛模拟。

在这个项目中,我们将把神经网络的权重建模为随机变量。为了确定最终的预测,我们将反复从这些权重的分布中进行采样,以获得预测的分布。

请注意,我们省略了一些数学细节。如果有兴趣,可以阅读更多关于变分推理的内容(www.cs.princeton.edu/courses/archive/fall11/cos597C/lectures/variational-inference-i.pdf)。

构建贝叶斯神经网络

对于这个项目,我们将使用德国交通标志数据集(benchmark.ini.rub.de/?section=gtsrb&amp;subsection=dataset)来构建贝叶斯神经网络。训练数据集包含 43 个类别的 26,640 张图像。类似地,测试数据集包含 12,630 张图像。

在执行代码之前,请阅读本书仓库中的README.md文件,安装相应的依赖项并了解如何运行代码。

以下是该数据集中存在的一张图像:

你可以看到数据集中有不同类别的交通标志。

我们首先对数据集进行预处理,使其符合学习算法的要求。通过直方图均衡化来调整图像的统一大小,这种方法用于增强对比度,然后将图像裁剪,专注于图像中的交通标志。此外,我们将图像转换为灰度图像,因为交通标志是通过形状而非颜色来识别的。

对于建模,我们定义了一个标准的 Lenet 模型(yann.lecun.com/exdb/lenet/),它是由 Yann Lecun 开发的。Lenet 是最早设计的卷积神经网络之一。它既小巧易懂,又足够大,能提供有趣的结果。

标准 Lenet 模型具有以下特点:

  • 三个卷积层,滤波器大小逐渐增加

  • 四个全连接层

  • 没有丢弃层

  • 每个全连接层或卷积层后使用线性整流函数ReLU

  • 每个卷积层后进行最大池化

我们训练该模型以最小化 ELBO 损失的负值,ELBO 损失在本章的理解 TensorFlow 概率、变分推理和蒙特卡洛方法一节中进行了定义。具体来说,我们将 ELBO 损失定义为两个项的组合:

  • 期望的对数似然或交叉熵,可以通过蒙特卡洛方法估计

  • KL 散度

模型训练完成后,我们将在保留数据集上评估预测。贝叶斯神经网络评估的一个主要区别是,我们无法从训练中获得一组固定的参数(模型的权重)。相反,我们获得所有参数的分布。在评估时,我们需要从每个参数的分布中采样值,以获得测试集的准确性。我们将多次采样模型的参数,以获得预测的置信区间。

最后,我们将在测试数据集中的一些样本图像中展示我们预测的不确定性,并绘制我们获得的权重参数的分布。

定义、训练和测试模型

benchmark.ini.rub.de/?section=gtsrb&amp;subsection=dataset下载训练和测试数据集。下载数据集后,接下来我们来看一下构建项目的步骤:

  1. 首先,通过直方图均衡化对数据集中存在的图像进行转换。这一步很重要,因为数据集中的每张图像可能具有不同的光照强度。从以下两张图片中可以看到,同样的交通标志在不同的光照下有很大的差异。直方图均衡化有助于标准化这些差异,使训练数据更加一致:

一旦完成均衡化操作,裁剪图像以仅聚焦于标志,并将图像大小调整为 32 x 32,以符合我们的学习算法要求:

请注意,我们使用 32 x 32 作为训练图像的形状,因为它足够大,可以保留图像的细微差别用于检测,同时又足够小,可以更快地训练模型。

def normalize_and_reshape_img(img):
# Histogram normalization in v channel
hsv = color.rgb2hsv(img)
hsv[:, :, 2] = exposure.equalize_hist(hsv[:, :, 2])
img = color.hsv2rgb(hsv)
# Crop of the centre
min_side = min(img.shape[:-1])
centre = img.shape[0] // 2, img.shape[1] // 2
img = img[centre[0] - min_side // 2:centre[0] + min_side // 2,
centre[1] - min_side // 2:centre[1] + min_side // 2,
:]
# Rescale to the desired size
img = transform.resize(img, (IMG_SIZE, IMG_SIZE))
return
 img
  1. 创建包含标签和图像信息的字典,并将其存储为 pickle 文件,这样我们就不必每次运行模型时都重新执行预处理代码。这意味着我们实际上是在预处理已转换的数据,以创建训练和测试数据集:
def preprocess_and_save_data(data_type ='train'):
'''
Preprocesses image data and saves the image features and labels as pickle files to be used for the model
:param data_type: data_type is 'train' or 'test'
:return: None
'''
if data_type =='train':
root_dir = os.path.join(DATA_DIR, 'GTSRB/Final_Training/Images/')
imgs = []
labels = []
all_img_paths = glob.glob(os.path.join(root_dir, '*/*.ppm'))
np.random.shuffle(all_img_paths)
for img_path in all_img_paths:
img = normalize_and_reshape_img(io.imread(img_path))
label = get_class(img_path)
imgs.append(img)
labels.append(label)
X_train = np.array(imgs, dtype='float32')
# Make one hot targets
Y_train = np.array(labels, dtype = 'uint8')
train_data = {"features": X_train, "labels": Y_train}
if not os.path.exists(os.path.join(DATA_DIR,"Preprocessed_Data")):
os.makedirs(os.path.join(DATA_DIR,"Preprocessed_Data"))
pickle.dump(train_data,open(os.path.join(DATA_DIR,"Preprocessed_Data","preprocessed_train.p"),"wb"))
return train_data
elif data_type == 'test':
# Reading the test file
test = pd.read_csv(os.path.join(DATA_DIR, "GTSRB", 'GT-final_test.csv'), sep=';')
X_test = []
y_test = []
i = 0
for file_name, class_id in zip(list(test['Filename']), list(test['ClassId'])):
img_path = os.path.join(DATA_DIR, 'GTSRB/Final_Test/Images/', file_name)
X_test.append(normalize_and_reshape_img(io.imread(img_path)))
y_test.append(class_id)
test_data = {"features": np.array(X_test,dtype ='float32'), "labels": np.array(y_test,dtype = 'uint8')}
if not os.path.exists(os.path.join(DATA_DIR,"Preprocessed_Data")):
os.makedirs(os.path.join(DATA_DIR,"Preprocessed_Data"))
pickle.dump(test_data,open(os.path.join(DATA_DIR,"Preprocessed_Data","preprocessed_test.p"),"wb"))
return test_data

在本项目中,我们将使用灰度图像,因为我们整个项目的任务是将交通标志图像分类为 43 个类别之一,并提供分类的不确定性度量。我们不关心图像的颜色。

  1. 使用 Keras 中的 LeNet 架构定义模型。最后,我们将 LeNet 模型输出的 43 维向量分配给 TensorFlow 概率中的分类分布函数(tfd.categorical)。这将帮助我们在后续生成预测的不确定性:
with tf.name_scope("BNN", values=[images]):
model = tf.keras.Sequential([
tfp.layers.Convolution2DFlipout(10,
kernel_size=5,
padding="VALID",
activation=tf.nn.relu),
tf.keras.layers.MaxPooling2D(pool_size=[3, 3],
strides=[1, 1],
padding="VALID"),
tfp.layers.Convolution2DFlipout(15,
kernel_size=3,
padding="VALID",
activation=tf.nn.relu),
tf.keras.layers.MaxPooling2D(pool_size=[2, 2],
strides=[2, 2],
padding="VALID"),
tfp.layers.Convolution2DFlipout(30,
kernel_size=3,
padding="VALID",
activation=tf.nn.relu),
tf.keras.layers.MaxPooling2D(pool_size=[2, 2],
strides=[2, 2],
padding="VALID"),
tf.keras.layers.Flatten(),
tfp.layers.DenseFlipout(400, activation=tf.nn.relu),
tfp.layers.DenseFlipout(120, activation = tf.nn.relu),
tfp.layers.DenseFlipout(84, activation=tf.nn.relu),
tfp.layers.DenseFlipout(43) ])
logits = model(images)
targets_distribution = tfd.Categorical(logits=logits)
  1. 我们定义损失以最小化 KL 散度直至 ELBO。计算本章《理解 TensorFlow 概率、变分推理和蒙特卡洛方法》部分中定义的 ELBO 损失。如您所见,我们使用model.losses属性来计算 KL 散度。这是因为 TensorFlow Keras Layer 的losses属性表示诸如正则化惩罚之类的副作用计算。与特定 TensorFlow 变量上的正则化惩罚不同,losses在这里表示 KL 散度的计算:
# Compute the -ELBO as the loss, averaged over the batch size.
neg_log_likelihood = -
  tf.reduce_mean(targets_distribution.log_prob(targets))
kl = sum(model.losses) / X_train.shape[0]
   elbo_loss = neg_log_likelihood + kl
  1. 使用 Adam 优化器,如第三章《使用 TensorFlow.js 在浏览器中进行情感分析》中所定义的,来优化 ELBO 损失:
with tf.name_scope("train"):
optimizer = tf.train.AdamOptimizer(learning_rate=LEARNING_RATE)
train_op = optimizer.minimize(elbo_loss)

请注意,我们使用 Adam 优化器,因为它通常在默认参数下比其他优化器表现更好。

  1. 使用以下参数训练模型:

    • 训练轮次 = 1,000

    • 批次大小 = 128

    • 学习率 = 0.001:

with tf.Session() as sess:
sess.run(init_op)
# Run the training loop.
train_handle = sess.run(train_iterator.string_handle())
test_handle = sess.run(test_iterator.string_handle())
for step in range(EPOCHS):
_ = sess.run([train_op, accuracy_update_op],
feed_dict={iter_handle: train_handle})
if step % 5== 0:
loss_value, accuracy_value = sess.run(
     [elbo_loss, accuracy], feed_dict={iter_handle: train_handle})
print("Epoch: {:>3d} Loss: {:.3f} Accuracy: {:.3f}".format(
step, loss_value, accuracy_value))
  1. 一旦模型训练完成,贝叶斯神经网络中的每个权重将具有分布而不是固定值。对每个权重进行多次采样(代码中为 50 次),并为每个样本获得不同的预测。尽管采样有用,但代价高昂。因此,我们应该仅在需要对预测结果的不确定性进行某种衡量时,才使用贝叶斯神经网络。以下是蒙特卡洛采样的代码:
#Sampling from the posterior and obtaining mean probability for held out dataset
probs = np.asarray([sess.run((targets_distribution.probs),
        feed_dict={iter_handle: test_handle})
for _ in range(NUM_MONTE_CARLO)])
  1. 一旦获得样本,计算每个测试数据集图像的平均概率,并像通常的机器学习分类器一样计算平均准确度。

    我们为这个数据集获得的平均准确度约为 ~ 89%(在 1,000 个训练轮次后)。您可以进一步调整参数或创建更深的模型来获得更好的准确度。

    下面是获取平均准确度的代码:

mean_probs = np.mean(probs, axis=0)
# Get the average accuracy
Y_pred = np.argmax(mean_probs, axis=1)
print("Overall Accuracy in predicting the test data = percent", round((Y_pred == y_test).mean() * 100,2))
  1. 下一步是计算每个测试图像的每个蒙特卡洛样本的准确度分布。为此,计算预测类别并与测试标签进行比较。预测类别可以通过将标签分配给具有最大概率的类别来获得,这基于给定网络参数的样本。这样,您可以获得准确度范围,并且还可以将这些准确度绘制在直方图上。以下是获取准确度并生成直方图的代码:
test_acc_dist = []
for prob in probs:
y_test_pred = np.argmax(prob, axis=1).astype(np.float32)
accuracy = (y_test_pred == y_test).mean() * 100
test_acc_dist.append(accuracy)
plt.hist(test_acc_dist)
plt.title("Histogram of prediction accuracies on test dataset")
plt.xlabel("Accuracy")
plt.ylabel("Frequency")
save_dir = os.path.join(DATA_DIR, "..", "Plots")
plt.savefig(os.path.join(save_dir, "Test_Dataset_Prediction_Accuracy.png"))

生成的直方图将类似于以下内容:

如您所见,我们有一个准确度的分布。这个分布可以帮助我们获得模型在测试数据集上的置信区间。

请注意,当您运行代码时,图形可能会有所不同,因为它是通过随机采样获得的。

  1. 从测试数据集中选取一些图像,并查看它们在蒙特卡洛方法中的不同样本预测。使用以下函数plot_heldout_prediction来生成来自不同样本的预测直方图:
def plot_heldout_prediction(input_vals, probs , fname, title=""):
save_dir = os.path.join(DATA_DIR, "..", "Plots")
fig = figure.Figure(figsize=(1, 1))
canvas = backend_agg.FigureCanvasAgg(fig)
ax = fig.add_subplot(1,1,1)
ax.imshow(input_vals.reshape((IMG_SIZE,IMG_SIZE)), interpolation="None")
canvas.print_figure(os.path.join(save_dir, fname + "_image.png"), format="png")
fig = figure.Figure(figsize=(10, 5))
canvas = backend_agg.FigureCanvasAgg(fig)
ax = fig.add_subplot(1,1,1)
#Predictions
y_pred_list = list(np.argmax(probs,axis=1).astype(np.int32))
bin_range = [x for x in range(43)]
ax.hist(y_pred_list,bins = bin_range)
ax.set_xticks(bin_range)
ax.set_title("Histogram of predicted class: " + title)
ax.set_xlabel("Class")
ax.set_ylabel("Frequency")
fig.tight_layout()
save_dir = os.path.join(DATA_DIR, "..", "Plots")
canvas.print_figure(os.path.join(save_dir, fname + "_predicted_class.png"), format="png")
print("saved {}".format(fname))

让我们看看一些图像及其预测结果:

对于前面的图像,所有的预测都属于正确的类别 02,如下图所示:

在以下两个案例中,尽管我们的平均预测是正确的,但在蒙特卡罗中,一些样本预测了错误的类别。你可以想象,在这样的情况下量化不确定性如何帮助自动驾驶汽车做出更好的道路决策。

案例 1:

在前面的图像中,一些蒙特卡罗的预测属于错误的类别,如下图所示:

案例 2:

在前面的图像中,一些蒙特卡罗的预测属于错误的类别,如下图所示:

案例 3:

在以下情况下,平均预测是错误的,但一些样本是正确预测的:

对于前面的图像,我们得到了以下的直方图:

案例 4:

显然,我们会遇到一些情况下预测不准确的样本:

对于此图像,我们得到了以下的直方图:

  1. 最后,直观地显示网络中权重的后验分布。在以下图表中,我们展示了网络中不同权重的后验均值和标准差:

对权重进行分布建模使我们能够为相同的图像开发预测,这在为预测构建置信区间时极为有用。

总结

正如我们所知道的,神经网络非常适合点预测,但无法帮助我们识别预测中的不确定性。另一方面,贝叶斯学习非常适合量化不确定性,但在多维度问题或大规模非结构化数据集(如图像)中扩展性较差。

在本章中,我们研究了如何通过贝叶斯神经网络将神经网络与贝叶斯学习结合起来。

我们使用了德国交通标志数据集,利用 Google 最近发布的工具 TensorFlow 概率,开发了一个贝叶斯神经网络分类器。TF 概率提供了高层次的 API 和函数来执行贝叶斯建模和推理。

我们在数据集上训练了 Lenet 模型。最后,我们使用蒙特卡罗方法从网络参数的后验分布中进行采样,以获得每个测试数据集样本的预测值,从而量化不确定性。

然而,我们仅仅触及了贝叶斯神经网络复杂性的表面。如果我们想要开发安全的人工智能,那么理解我们预测中的不确定性至关重要。

在下一章,我们将学习一个在机器学习中新的概念——自编码器。我们将探讨如何利用它们来检测信用卡欺诈。

问题

  1. 什么是 TensorFlow 概率?

  2. 变分推断是什么,为什么它很重要?

  3. 什么是 KL 散度?

  4. 神经网络的权重上的先验和后验是什么意思?

第九章:使用 DiscoGAN 从鞋图像生成匹配的鞋包

人类在理解不同领域之间的关系方面相当聪明。例如,我们可以轻松理解西班牙语句子及其英文翻译之间的关系。我们甚至能猜出穿什么颜色的领带来搭配某种西装。虽然这对人类来说似乎很简单,但对机器来说却不是一个直接的过程。

跨领域的风格迁移任务可以被框定为一个条件图像生成问题。给定一个来自某个领域的图像,我们能否学会将其映射到来自另一个领域的图像?

尽管有许多方法尝试使用来自两个不同领域的成对标注数据来实现这一目标,但这些方法充满了问题。这些方法的主要问题在于获取成对标注数据,这既是一个昂贵又耗时的过程。

在本章中,我们将学习一种无需明确提供成对标注数据给算法的风格迁移方法。这种方法被称为 DiscoGAN,最近由 Kim 等人在论文《利用生成对抗网络学习发现跨域关系》中提出 (arxiv.org/pdf/1703.05192.pdf)。具体来说,我们将尝试从鞋包图像中生成匹配的鞋子。

本章的剩余部分组织结构如下:

  • 生成对抗网络(GANs) 介绍

  • 什么是 DiscoGAN?

  • 如何从鞋包图像生成匹配的鞋子,反之亦然

理解生成模型

一种无监督学习模型,它学习训练集的基础数据分布并生成可能具有或不具有变化的新数据,通常被称为生成模型。知道真实的基础分布可能并不总是可行,因此神经网络会训练一个尽可能接近真实分布的函数。

训练生成模型最常用的方法如下:

  • 变分自编码器(VAE): 一个高维输入图像通过自编码器编码,创建一个低维的表示。在这个过程中,最重要的是保持数据的基础分布。这个编码器只能通过解码器来映射输入图像,不能引入任何变异来生成相似的图像。VAE 通过生成受约束的潜在向量来引入变异,这些向量仍然遵循基础分布。尽管 VAE 有助于创建概率图模型,但生成的图像往往会略显模糊。

  • PixelRNN/PixelCNN:这些自回归模型用于训练能够建模条件分布的网络,给定从左上角开始的前几个像素,预测下一个像素。RNNs 可以在图像中横向和纵向移动。PixelRNN 的训练过程稳定且简单,相较于其他模型,它具有更好的对数似然值,但训练时间较长且效率较低。

  • 生成对抗网络:生成对抗网络最早由 Goodfellow 等人 在 2014 年的论文中提出 (arxiv.org/abs/1406.2661)。它们可以被看作是一个竞争框架,包含两个对立的部分:生成器和判别器。这两个部分实际上是两个可微分的神经网络函数。生成器接受一个随机生成的输入,称为潜在样本,并生成一张图像。生成器的整体目标是生成一张尽可能接近真实输入图像(如 MNIST 数字)的图像,并将其作为输入提供给判别器。

判别器本质上是一个分类器,经过训练后用于区分真实图像(原始的 MNIST 数字)和伪造图像(生成器的输出)。理想情况下,经过训练后,生成器应当调整其参数,捕捉潜在的训练数据分布,并让判别器误认为输入是一个真实图像。

让我们考虑一个来自现实世界的类比。想象 GAN 的工作方式就像伪造者制造伪钞和警察识别并销毁这些伪钞之间的关系。伪造者的目的是试图将伪钞当作真钱在市场上流通。这就类似于生成器的工作。警察会检查每一张钞票,接受真实的钞票,销毁伪钞。警察了解真实钞票的特征,并将其与待检验钞票的属性进行比较,以判断其真伪。如果匹配,钞票会被保留,否则就会被销毁。这与判别器的工作原理类似。

训练 GAN

下图展示了 GAN 的基本架构:

使用随机输入来生成数据样本。例如,生成器 G(z) 使用先验分布 p(z) 来生成输入 z,然后使用 z 生成一些数据。这个输出作为输入被传递到判别器神经网络 D(x)。它从  中获取输入 x,其中  是我们的真实数据分布。然后,D(x) 使用 sigmoid 函数解决一个二分类问题,输出结果在 0 到 1 之间。

GANs 的训练是生成器和判别器之间的竞争过程。目标函数可以通过以下数学形式表示:

在此,以下适用:

  • 表示判别器的参数

  • 表示生成器的参数

  • 表示训练数据的基础分布

  • 表示判别器对输入图像 x 的操作

  • 表示生成器在潜在样本 z 上的操作

  • 表示生成的假数据的判别器输出

在目标函数中,左边的第一个项表示判别器输出来自真实分布的交叉熵()。左边的第二个项是随机分布()与判别器对生成器输出的预测(该输出是使用来自 的随机样本 z 生成的)的交叉熵。判别器试图最大化这两个项,分别将图像分类为真实和假的。另一方面,生成器则试图通过最小化此目标来欺骗判别器。

为了训练 GAN,使用基于梯度的优化算法,例如随机梯度下降。算法流程如下:

  1. 首先,采样 m 个噪声样本和 m 个真实数据样本。

  2. 冻结生成器,即将训练设置为 false,以便生成器网络仅进行前向传播而不进行反向传播。然后对这些数据训练判别器。

  3. 对不同的 m 噪声样本进行采样。

  4. 冻结判别器,并在这些数据上训练生成器。

  5. 重复执行前述步骤。

正式来说,伪代码如下所示。

在这个示例中,我们正在对生成对抗网络进行小批量随机梯度下降训练。应用于判别器的步数 k 是一个超参数。我们在实验中使用了 k=1,这是最不昂贵的选项:

GAN 训练的伪代码。对于 k=1,这等价于依次训练 D 和 G。改编自 Goodfellow 等人,2014 年

基于梯度的更新可以使用任何标准的梯度学习规则。我们在实验中使用了动量。

应用

GANs 的一些应用包括将单色或黑白图像转换为彩色图像、在图像中填充附加细节,如将物体插入到部分图像或仅包含边缘的图像中,以及构建一个人的老年形象,基于其当前的图像。

挑战

尽管 GANs 能够从给定的输入数据生成非常清晰的图像,但由于训练动态的不稳定,其优化是很难实现的。它们还面临其他挑战,如模式崩溃和初始化问题。模式崩溃是指当数据是多模态时,生成器从未被激励去覆盖所有模式,导致生成样本的变化性较低,从而降低了 GANs 的实用性。如果所有生成的样本开始变得相同,就会导致完全崩溃。在大多数样本表现出一些共性时,模型会出现部分崩溃。在这一过程中,GANs 通过一个旨在实现最小最大优化的目标函数进行工作,但如果初始参数无效,就会变成一个无真正优化的振荡过程。此外,还有像 GANs 无法区分在某个位置应该出现的特定物体数量等问题。例如,GANs 无法理解不能有超过两个眼睛,并且可能生成带有三个眼睛的人脸图像。还有 GANs 无法适应三维视角的问题,比如前视图和后视图。这会导致产生平面 2D 图像,而不是三维物体的深度效果。

不同的 GAN 变体随着时间的推移不断发展。以下是其中的一些:

  • 深度卷积生成对抗网络 (DCGANs) 是 GAN 架构的一次重要改进。它由卷积层组成,避免了最大池化或全连接层的使用。该架构主要使用卷积步幅和反卷积来进行下采样和上采样。它还在生成器中使用 ReLU 激活函数,在判别器中使用 LeakyReLU。

  • InfoGANs 是另一种 GAN 变体,它尝试将图像的有意义特征(例如旋转)编码到噪声向量 z 的部分中。

  • 条件生成对抗网络 (cGANs) 使用额外的条件信息,描述数据的某些方面,作为输入提供给生成器和判别器。例如,如果我们处理的是车辆,条件信息可能描述四轮或两轮等属性。这有助于生成更好的样本和附加特征。在本章中,我们将主要关注 DiscoGAN,它将在接下来的部分中进行描述。

了解 DiscoGANs

在本节中,我们将重点深入了解 Discovery GANs,它们通常被称为DiscoGANs

在深入研究之前,让我们先尝试理解机器学习中的重构损失,因为这是本章主要依赖的概念之一。当我们学习无结构数据类型(如图像/文本)的表示时,我们希望模型以一种方式对数据进行编码,使得当它被解码时,可以恢复出底层的图像/文本。为了在模型中显式地加入这个条件,我们在训练模型时使用重构损失(本质上是重构图像与原始图像之间的欧几里得距离)。

风格迁移一直是 GAN 最显著的应用之一。风格迁移基本上是指在给定一个领域中的图片/数据的情况下,是否可以成功生成另一个领域中的图片/数据。这个问题在许多研究人员中变得非常著名。

你可以从 Jing 等人的论文《神经风格迁移:综述》(arxiv.org/abs/1705.04058)中了解更多关于风格迁移问题的内容。然而,大多数工作都是通过使用一个由人工或其他算法生成的显式配对数据集来完成的。这给这些方法带来了限制,因为配对数据很少能够获得,而且生成配对数据的成本太高。

另一方面,DiscoGAN 提出了一种学习跨领域关系的方法,无需显式的配对数据集。该方法从一个领域中获取一张图片,并生成另一个领域中的对应图片。假设我们正尝试将 Domain A 中的图片迁移到 Domain B。在学习过程中,我们通过重构损失强制生成的图像成为 Domain A 图像的图像表示,并通过 GAN 损失使其尽可能接近 Domain B 中的图像,如前所述。本质上,这种方法倾向于在两个领域之间生成一个双射(一对一)映射,而不是多对一或一对多的映射。

DiscoGAN 的基本单元

如前所述,普通的 GAN 包含生成器和判别器。我们来尝试理解 DiscoGAN 的构建块,然后再继续理解如何将它们结合起来,从而学习跨领域关系。具体包括以下内容:

  • 生成器: 在原始 GAN 中,生成器会从一个输入向量z(例如,随机从高斯分布中采样)生成伪造的图像。然而,在这种情况下,由于我们希望将图像从一个领域转移到另一个领域,我们将输入向量z替换为一张图片。以下是生成器函数的参数:
参数
输入图片尺寸 64x64x3
输出图片尺寸 64x64x3
# 卷积层 4
# 卷积转置/反卷积层 4
归一化函数 批量归一化
激活函数 LeakyReLU

在指定每个特定层的结构之前,让我们先尝试理解一下在参数中提到的几个术语。

  • 转置卷积:正如我们之前提到的,生成器用于从输入向量生成图像。在我们的案例中,输入图像首先通过 4 层卷积层进行卷积,生成一个嵌入。通过上采样将低分辨率图像转换为高分辨率图像,从嵌入中生成图像。

常见的上采样方法包括手动特征工程,通过插值来处理低维度图像。一种更好的方法是采用转置卷积,也称为分数步幅卷积/反卷积。它不使用任何预定义的插值方法。假设我们有一个 4x4 矩阵,它与一个 3x3 的滤波器进行卷积(步幅为 1,且不做填充);这将得到一个 2x2 的矩阵。如你所见,我们将原始图像从 4x4 降采样到 2x2。从 2x2 恢复到 4x4 的过程可以通过转置卷积实现。

从实现角度来看,TensorFlow 中用于定义卷积层的内置函数可以直接与num_outputs值一起使用,num_outputs可以进行更改以执行上采样。

  • 批量归一化: 这种方法用于对抗深度神经网络中发生的内部协方差偏移。

协方差偏移可以定义为当模型能够预测输入分布变化时。例如,假设我们训练一个模型来检测黑白狗的图像。在推理阶段,如果我们向模型提供彩色狗的图像,模型的表现将不佳。这是因为模型根据黑白图像学习了参数,而这些参数不适用于预测彩色图像。

深度神经网络会经历所谓的内部协方差偏移,因为内部层参数的变化会改变下一层输入的分布。为了解决这个问题,我们通过使用每个批次的均值和方差来规范化输出,并将均值和方差的加权组合传递给下一层。由于加权组合,批量归一化在每一层的神经网络中添加了两个额外的参数。

批量归一化有助于加速训练,并因其正则化效果而倾向于减少过拟合。

在这个模型中,我们在所有卷积层和卷积转置层中使用批量归一化,除了第一层和最后一层。

  • Leaky ReLU: ** ReLU(或称修正线性单元**)在深度学习领域作为激活函数非常流行。深度神经网络中的 ReLU 单元有时会变得脆弱,因为它们可能导致神经元死亡或在任何数据点上都无法再次激活。下图展示了 ReLU 函数:

使用 Leaky ReLU 来尝试解决这个问题。对于负输入值,它们具有小的负值,而不是零。这避免了神经元死掉的问题。以下图示展示了一个 Leaky ReLU 函数的示例:

  • 判别器: 在我们之前描述的 GAN 中,生成器从一个随机采样的输入向量(比如高斯分布)生成假图像。然而,在这里,由于我们希望将图像从一个领域转移到另一个领域,我们将输入向量替换为一张图像。

从架构角度来看,判别器的参数如下:

    • 层: 判别器由 5 个卷积层组成,每个卷积层堆叠在一起,然后是两个全连接层。

    • 激活函数: 所有层使用 Leaky ReLU 激活,除了最后一层全连接层。最后一层使用sigmoid来预测样本的概率。

    • 归一化器: 该操作执行批量归一化,但不包括网络的第一层和最后一层。

    • 步幅: 所有卷积层的步幅长度为 2。

DiscoGAN 建模

对于每个映射,也就是手袋(用b表示)到鞋子(用s表示),或者反过来,我们加入两个生成器。假设,对于映射bs,第一个生成器将输入图像从b领域映射到s领域,而第二个生成器将图像从s领域重建到b领域。直观上,我们需要第二个生成器来实现我们在前面章节中提到的一一映射目标。数学上,可以表示如下:

在建模时,由于这是一个非常难以满足的约束,我们加入了重建损失。重建损失如下所示:

现在,生成假图像的常见 GAN 损失如下所示:

对于每个映射,生成器接收两个损失:

  • 重建损失,旨在查看我们如何能够将生成的图像映射到其原始领域

  • 通常的 GAN 损失用于欺骗判别器的任务

在这种情况下,判别器是常见的判别器,使用我们在训练 GAN部分提到的损失。我们用 表示它。

生成器和判别器的总损失如下所示:

构建一个 DiscoGAN 模型

本问题中的基本数据集来自edges2handbags(people.eecs.berkeley.edu/~tinghuiz/projects/pix2pix/datasets/edges2handbags.tar.gz)和edges2shoes(people.eecs.berkeley.edu/~tinghuiz/projects/pix2pix/datasets/edges2shoes.tar.gz)数据集。这些数据集中的每张图像都包含两个子图像。一个是物体的彩色图像,另一个是对应彩色图像的边缘图像。

按照以下步骤构建 DiscoGAN 模型:

  1. 首先,调整和裁剪此数据集中的图像,以获取手袋和鞋子图像:
def extract_files(*data_dir*,*type* = 'bags'):
   '''
 :param data_dir: Input directory
 :param type: bags or shoes
   :return: saves the cropped files to the bags to shoes directory
   '''

   input_file_dir = os.path.join(os.getcwd(),*data_dir*, "train")
   result_dir = os.path.join(os.getcwd(),*type*)
   *if not* os.path.exists(result_dir):
       os.makedirs(result_dir)

   file_names= os.listdir(input_file_dir)
   *for* file *in* file_names:
       input_image = Image.open(os.path.join(input_file_dir,file))
       input_image = input_image.resize([128, 64])
       input_image = input_image.crop([64, 0, 128, 64])  # Cropping only the colored image. Excluding the edge image
       input_image.save(os.path.join(result_dir,file))
  1. 将图像保存在bagsshoes的相应文件夹中。一些示例图像如下所示:
鞋子 包包
  1. 实现generator函数,包含 4 个卷积层,后跟 4 个卷积转置(或反卷积)层。该场景中使用的卷积核大小为 4,而卷积层和反卷积层的stride分别为21。所有层的激活函数均使用 Leaky Relu。该函数的代码如下:
def generator(x, initializer, s*cope_name* = 'generator',*reuse*=*False*):
   *with* tf.variable_scope(*scope_name*) *as* scope:
       *if* *reuse*:
           scope.reuse_variables()
       conv1 = tf.contrib.layers.conv2d(inputs=*x*, num_outputs=32, kernel_size=4, stride=2, padding="SAME",reuse=*reuse*, activation_fn=tf.nn.leaky_relu, weights_initializer=*initializer*,
                                        scope="disc_conv1")  # 32 x 32 x 32
       conv2 = tf.contrib.layers.conv2d(inputs=conv1, num_outputs=64, kernel_size=4, stride=2, padding="SAME",
                                        reuse=*reuse*, activation_fn=tf.nn.leaky_relu, normalizer_fn=tf.contrib.layers.batch_norm,
                                        weights_initializer=*initializer*, scope="disc_conv2")  # 16 x 16 x 64
       conv3 = tf.contrib.layers.conv2d(inputs=conv2, num_outputs=128, kernel_size=4, stride=2, padding="SAME",
                                        reuse=*reuse*, activation_fn=tf.nn.leaky_relu, normalizer_fn=tf.contrib.layers.batch_norm,
                                        weights_initializer=*initializer*, scope="disc_conv3")  # 8 x 8 x 128
       conv4 = tf.contrib.layers.conv2d(inputs=conv3, num_outputs=256, kernel_size=4, stride=2, padding="SAME",
                                        reuse=*reuse*, activation_fn=tf.nn.leaky_relu, normalizer_fn=tf.contrib.layers.batch_norm,
                                        weights_initializer=*initializer*, scope="disc_conv4")  # 4 x 4 x 256

       deconv1 = tf.contrib.layers.conv2d(conv4, num_outputs=4 * 128, kernel_size=4, stride=1, padding="SAME",
                                              activation_fn=tf.nn.relu, normalizer_fn=tf.contrib.layers.batch_norm,
                                              weights_initializer=*initializer*, scope="gen_conv1")
       deconv1 = tf.reshape(deconv1, shape=[tf.shape(*x*)[0], 8, 8, 128])

       deconv2 = tf.contrib.layers.conv2d(deconv1, num_outputs=4 * 64, kernel_size=4, stride=1, padding="SAME",
                                              activation_fn=tf.nn.relu, normalizer_fn=tf.contrib.layers.batch_norm,
                                              weights_initializer=*initializer*, scope="gen_conv2")
       deconv2 = tf.reshape(deconv2, shape=[tf.shape(*x*)[0], 16, 16, 64])

       deconv3 = tf.contrib.layers.conv2d(deconv2, num_outputs=4 * 32, kernel_size=4, stride=1, padding="SAME",
                                              activation_fn=tf.nn.relu, normalizer_fn=tf.contrib.layers.batch_norm,
                                              weights_initializer=*initializer*, scope="gen_conv3")
       deconv3 = tf.reshape(deconv3, shape=[tf.shape(*x*)[0], 32, 32, 32])

       deconv4 = tf.contrib.layers.conv2d(deconv3, num_outputs=4 * 16, kernel_size=4, stride=1, padding="SAME",
                                              activation_fn=tf.nn.relu, normalizer_fn=tf.contrib.layers.batch_norm,
                                              weights_initializer=*initializer*, scope="gen_conv4")
       deconv4 = tf.reshape(deconv4, shape=[tf.shape(*x*)[0], 64, 64, 16])

       recon = tf.contrib.layers.conv2d(deconv4, num_outputs=3, kernel_size=4, stride=1, padding="SAME", \
                                            activation_fn=tf.nn.relu, scope="gen_conv5")

       return recon

  1. 使用我们在DiscoGAN 的基本单元部分之前提到的参数定义判别器:
def discriminator(*x*,*initializer*, *scope_name* ='discriminator',  *reuse*=*False*):
   *with* tf.variable_scope(*scope_name*) *as* scope:
       *if* *reuse*:
           scope.reuse_variables()
       conv1 = tf.contrib.layers.conv2d(inputs=*x*, num_outputs=32, kernel_size=4, stride=2, padding="SAME",
                                        reuse=*reuse*, activation_fn=tf.nn.leaky_relu, weights_initializer=*initializer*,
                                        scope="disc_conv1")  # 32 x 32 x 32
       conv2 = tf.contrib.layers.conv2d(inputs=conv1, num_outputs=64, kernel_size=4, stride=2, padding="SAME",
                                        reuse=*reuse*, activation_fn=tf.nn.leaky_relu, normalizer_fn=tf.contrib.layers.batch_norm,
                                        weights_initializer=*initializer*, scope="disc_conv2")  # 16 x 16 x 64
       conv3 = tf.contrib.layers.conv2d(inputs=conv2, num_outputs=128, kernel_size=4, stride=2, padding="SAME",
                                        reuse=*reuse*, activation_fn=tf.nn.leaky_relu, normalizer_fn=tf.contrib.layers.batch_norm,
                                        weights_initializer=*initializer*, scope="disc_conv3")  # 8 x 8 x 128
       conv4 = tf.contrib.layers.conv2d(inputs=conv3, num_outputs=256, kernel_size=4, stride=2, padding="SAME",
                                        reuse=*reuse*, activation_fn=tf.nn.leaky_relu, normalizer_fn=tf.contrib.layers.batch_norm,
                                        weights_initializer=*initializer*, scope="disc_conv4")  # 4 x 4 x 256
       conv5 = tf.contrib.layers.conv2d(inputs=conv4, num_outputs=512, kernel_size=4, stride=2, padding="SAME",
                                        reuse=*reuse*, activation_fn=tf.nn.leaky_relu, normalizer_fn=tf.contrib.layers.batch_norm,
                                        weights_initializer=*initializer*, scope="disc_conv5")  # 2 x 2 x 512
       fc1 = tf.reshape(conv5, shape=[tf.shape(*x*)[0], 2 * 2 * 512])
       fc1 = tf.contrib.layers.fully_connected(inputs=fc1, num_outputs=512, reuse=*reuse*, activation_fn=tf.nn.leaky_relu,
                                               normalizer_fn=tf.contrib.layers.batch_norm,
                                               weights_initializer=*initializer*, scope="disc_fc1")
       fc2 = tf.contrib.layers.fully_connected(inputs=fc1, num_outputs=1, reuse=*reuse*, activation_fn=tf.nn.sigmoid,
                                               weights_initializer=*initializer*, scope="disc_fc2")
       return fc2
  1. 使用以下define_network函数,该函数为每个领域定义了两个生成器和两个判别器。在该函数中,generatordiscriminator的定义与我们在前一步使用函数时定义的一样。然而,对于 DiscoGAN,函数定义了一个generator,它生成另一个领域的假图像,一个generator负责重建。另外,discriminators为每个领域中的真实图像和假图像定义。该函数的代码如下:
def define_network(self):
   # generators
   # This one is used to generate fake data
   self.gen_b_fake = generator(self.X_shoes, self.initializer,scope_name="generator_sb")
   self.gen_s_fake =   generator(self.X_bags, self.initializer,scope_name="generator_bs")
   # Reconstruction generators
   # Note that parameters are being used from previous layers
   self.gen_recon_s = generator(self.gen_b_fake, self.initializer,scope_name="generator_sb",  reuse=*True*)
   self.gen_recon_b = generator(self.gen_s_fake,  self.initializer, scope_name="generator_bs", reuse=*True*)
   # discriminator for Shoes
   self.disc_s_real = discriminator(self.X_shoes,self.initializer, scope_name="discriminator_s")
   self.disc_s_fake = discriminator(self.gen_s_fake,self.initializer, scope_name="discriminator_s", reuse=*True*)
   # discriminator for Bags
   self.disc_b_real = discriminator(self.X_bags,self.initializer,scope_name="discriminator_b")
   self.disc_b_fake = discriminator(self.gen_b_fake, self.initializer, reuse=*True*,scope_name="discriminator_b")
  1. 让我们定义我们在DiscoGAN 建模部分之前定义的loss函数。以下define_loss函数定义了基于重建图像和原始图像之间欧几里得距离的重建损失。为了生成 GAN 和判别器损失,该函数使用交叉熵函数:
def define_loss(self):
   # Reconstruction loss for generators
   self.const_loss_s = tf.reduce_mean(tf.losses.mean_squared_error(self.gen_recon_s, self.X_shoes))
   self.const_loss_b = tf.reduce_mean(tf.losses.mean_squared_error(self.gen_recon_b, self.X_bags))
   # generator loss for GANs
   self.gen_s_loss = tf.reduce_mean(
       tf.nn.sigmoid_cross_entropy_with_logits(logits=self.disc_s_fake, labels=tf.ones_like(self.disc_s_fake)))
   self.gen_b_loss = tf.reduce_mean(
       tf.nn.sigmoid_cross_entropy_with_logits(logits=self.disc_b_fake, labels=tf.ones_like(self.disc_b_fake)))
   # Total generator Loss
   self.gen_loss =  (self.const_loss_b + self.const_loss_s)  + self.gen_s_loss + self.gen_b_loss
   # Cross Entropy loss for discriminators for shoes and bags
   # Shoes
   self.disc_s_real_loss = tf.reduce_mean(
       tf.nn.sigmoid_cross_entropy_with_logits(logits=self.disc_s_real, labels=tf.ones_like(self.disc_s_real)))
   self.disc_s_fake_loss = tf.reduce_mean(
       tf.nn.sigmoid_cross_entropy_with_logits(logits=self.disc_s_fake, labels=tf.zeros_like(self.disc_s_fake)))
   self.disc_s_loss = self.disc_s_real_loss + self.disc_s_fake_loss  # Combined
   # Bags
   self.disc_b_real_loss = tf.reduce_mean(
       tf.nn.sigmoid_cross_entropy_with_logits(logits=self.disc_b_real, labels=tf.ones_like(self.disc_b_real)))
   self.disc_b_fake_loss = tf.reduce_mean(
       tf.nn.sigmoid_cross_entropy_with_logits(logits=self.disc_b_fake, labels=tf.zeros_like(self.disc_b_fake)))
   self.disc_b_loss = self.disc_b_real_loss + self.disc_b_fake_loss
   # Total discriminator Loss
   self.disc_loss = self.disc_b_loss + self.disc_s_loss
  1. 使用在第三章《使用 TensorFlow.js 在浏览器中进行情感分析》中定义的AdamOptimizer,并实现以下define_optimizer函数:
*def* define_optimizer(self):
   self.disc_optimizer = tf.train.AdamOptimizer(LEARNING_RATE).minimize(self.disc_loss, var_list=self.disc_params)
   self.gen_optimizer = tf.train.AdamOptimizer(LEARNING_RATE).minimize(self.gen_loss, var_list=self.gen_params)
  1. 为了调试,将摘要写入日志文件。虽然你可以在摘要中添加任何内容,但下面的summary_函数仅添加所有的损失,以便观察各种损失随时间变化的曲线。该函数的代码如下:
*def* summary_(self):
   # Store the losses
   tf.summary.scalar("gen_loss", self.gen_loss)
   tf.summary.scalar("gen_s_loss", self.gen_s_loss)
   tf.summary.scalar("gen_b_loss", self.gen_b_loss)
   tf.summary.scalar("const_loss_s", self.const_loss_s)
   tf.summary.scalar("const_loss_b", self.const_loss_b)
   tf.summary.scalar("disc_loss", self.disc_loss)
   tf.summary.scalar("disc_b_loss", self.disc_b_loss)
   tf.summary.scalar("disc_s_loss", self.disc_s_loss)

   # Histograms for all vars
   *for* var *in* tf.trainable_variables():
       tf.summary.histogram(var.name, var)

   self.summary_ = tf.summary.merge_all()
  1. 为训练模型定义以下参数:
    • 批量大小:256

    • 学习率:0.0002

    • Epochs = 100,000(如果没有得到期望的结果,可以使用更多的 epoch)

  1. 使用以下代码训练模型。这里是它的简要说明:

    1. 对于每个 Epoch,代码会获取鞋子和包的小批量图像。它将小批量图像传递给模型,首先更新判别器损失。

    2. 再次为鞋子和包进行小批量采样,并在保持判别器参数固定的情况下更新生成器损失。

    3. 每 10 个 epoch,它会将总结写入 Tensorboard。

    4. 每 1000 个 epoch,它会随机从包和鞋子数据集中采样 1 张图像,并保存重建图像和伪造图像,以便可视化。

    5. 此外,每 1000 个 epoch,它会保存模型,这对于你想在某个时刻恢复训练非常有用。

print ("Starting Training")
*for* global_step *in* range(start_epoch,EPOCHS):
   shoe_batch = get_next_batch(BATCH_SIZE,"shoes")
   bag_batch = get_next_batch(BATCH_SIZE,"bags")
   feed_dict_batch = {*model*.X_bags: bag_batch, *model*.X_shoes: shoe_batch}
   op_list = [*model*.disc_optimizer, *model*.gen_optimizer, *model*.disc_loss, *model*.gen_loss, *model*.summary_]
   _, _, disc_loss, gen_loss, summary_ = sess.run(op_list, feed_dict=feed_dict_batch)
   shoe_batch = get_next_batch(BATCH_SIZE, "shoes")
   bag_batch = get_next_batch(BATCH_SIZE, "bags")
   feed_dict_batch = {*model*.X_bags: bag_batch, *model*.X_shoes: shoe_batch}
   _, gen_loss = sess.run([*model*.gen_optimizer, *model*.gen_loss], feed_dict=feed_dict_batch)
   *if* global_step%10 ==0:
       train_writer.add_summary(summary_,global_step)
   *if* global_step%100 == 0:
       print("EPOCH:" + str(global_step) + "\tgenerator Loss: " + str(gen_loss) + "\tdiscriminator Loss: " + str(disc_loss))
   *if* global_step % 1000 == 0:
       shoe_sample = get_next_batch(1, "shoes")
       bag_sample = get_next_batch(1, "bags")
       ops = [*model*.gen_s_fake, *model*.gen_b_fake, *model*.gen_recon_s, *model*.gen_recon_b]
       gen_s_fake, gen_b_fake, gen_recon_s, gen_recon_b = sess.run(ops, feed_dict={*model*.X_shoes: shoe_sample, *model*.X_bags: bag_sample})
       save_image(global_step, gen_s_fake, str("gen_s_fake_") + str(global_step))
       save_image(global_step,gen_b_fake, str("gen_b_fake_") + str(global_step))
       save_image(global_step, gen_recon_s, str("gen_recon_s_") + str(global_step))
       save_image(global_step, gen_recon_b, str("gen_recon_b_") + str(global_step))
   *if* global_step % 1000 == 0:
       *if not* os.path.exists("./model"):
           os.makedirs("./model")
       saver.save(sess, "./model" + '/model-' + str(global_step) + '.ckpt')
       print("Saved Model")
print ("Starting Training")
*for* global_step *in* range(start_epoch,EPOCHS):
   shoe_batch = get_next_batch(BATCH_SIZE,"shoes")
   bag_batch = get_next_batch(BATCH_SIZE,"bags")
   feed_dict_batch = {*model*.X_bags: bag_batch, *model*.X_shoes: shoe_batch}
   op_list = [*model*.disc_optimizer, *model*.gen_optimizer, *model*.disc_loss, *model*.gen_loss, *model*.summary_]
   _, _, disc_loss, gen_loss, summary_ = sess.run(op_list, feed_dict=feed_dict_batch)
   shoe_batch = get_next_batch(BATCH_SIZE, "shoes")
   bag_batch = get_next_batch(BATCH_SIZE, "bags")
   feed_dict_batch = {*model*.X_bags: bag_batch, *model*.X_shoes: shoe_batch}
   _, gen_loss = sess.run([*model*.gen_optimizer, *model*.gen_loss], feed_dict=feed_dict_batch)
   *if* global_step%10 ==0:
       train_writer.add_summary(summary_,global_step)
   *if* global_step%100 == 0:
       print("EPOCH:" + str(global_step) + "\tgenerator Loss: " + str(gen_loss) + "\tdiscriminator Loss: " + str(disc_loss))
   *if* global_step % 1000 == 0:
       shoe_sample = get_next_batch(1, "shoes")
       bag_sample = get_next_batch(1, "bags")
       ops = [*model*.gen_s_fake, *model*.gen_b_fake, *model*.gen_recon_s, *model*.gen_recon_b]
       gen_s_fake, gen_b_fake, gen_recon_s, gen_recon_b = sess.run(ops, feed_dict={*model*.X_shoes: shoe_sample, *model*.X_bags: bag_sample})
       save_image(global_step, gen_s_fake, str("gen_s_fake_") + str(global_step))
       save_image(global_step,gen_b_fake, str("gen_b_fake_") + str(global_step))
       save_image(global_step, gen_recon_s, str("gen_recon_s_") + str(global_step))
       save_image(global_step, gen_recon_b, str("gen_recon_b_") + str(global_step))
   *if* global_step % 1000 == 0:
       *if not* os.path.exists("./model"):
           os.makedirs("./model")
       saver.save(sess, "./model" + '/model-' + str(global_step) + '.ckpt')

       print("Saved Model")

我们在一块 GTX 1080 显卡上进行了训练,花费了大量时间。如果可能,强烈建议使用处理能力更强的 GPU。

总结

在本章中,我们首先了解了 GAN 是什么。它们是一种新的生成模型,帮助我们生成新的图像。

我们还提到了其他类型的生成模型,如变分自编码器(Variational Auto-encoders)和 PixelRNN,以概览不同种类的生成模型。我们还讨论了不同种类的 GAN,回顾了自 2014 年 GAN 第一篇论文发布以来该领域的进展。

接下来,我们了解了 DiscoGAN,这是一种新的 GAN 类型,可以帮助我们了解跨领域的关系。具体来说,在本章中,我们的重点是构建一个模型,从鞋子生成手袋图像,反之亦然。

最后,我们了解了 DiscoGAN 的架构以及它们与常规 GAN 的不同之处。

在下一章中,我们将学习如何在 Fashion MNIST 数据集上实现胶囊网络。

问题

  • 你能改变训练参数,如学习率、批量大小,并观察重建图像和伪造图像的质量变化吗?

  • 你能在 Tensorboard 中可视化存储的总结,以便理解学习过程吗?

  • 你能想到其他可以用于风格迁移的数据集吗?

第十章:使用胶囊网络分类服装图像

在本章中,我们将学习如何在 Fashion MNIST 数据集上实现胶囊网络。本章将涵盖胶囊网络的内部工作原理,并解释如何在 TensorFlow 中实现它们。你还将学习如何评估和优化模型。

我们选择胶囊网络是因为它们具有保留图像空间关系的能力。胶囊网络由 Geoff Hinton 等人提出。他们在 2017 年发表了一篇论文,可以在arxiv.org/abs/1710.09829找到。胶囊网络作为一种新的神经网络类型,在深度学习社区中获得了极大的关注。

到本章结束时,我们将能够通过以下内容使用胶囊网络进行服装分类:

  • 理解胶囊网络的重要性

  • 对胶囊的简要理解

  • 通过协议路由算法

  • 实现 CapsNet 架构来分类 Fashion-MNIST 图像

  • 胶囊网络的局限性

理解胶囊网络的重要性

卷积神经网络CNNs)构成了当前所有图像检测重大突破的基础。CNN 通过检测网络低层中存在的基本特征,然后继续检测网络高层中存在的更高级特征来工作。这种设置没有包含低级特征之间的姿势(平移和旋转)关系,而这些低级特征构成了任何复杂物体。

想象一下试图识别一张面孔。在这种情况下,仅仅有眼睛、鼻子和耳朵在图像中,可能会导致 CNN 认为它是面孔,而不关心相关物体的相对方向。为了进一步解释这一点,如果图像中鼻子在眼睛上方,CNN 仍然可以检测出它是图像。CNN 通过使用最大池化来解决这个问题,帮助提高更高层的视野范围。然而,这个操作并不是完美的解决方案,因为我们使用它时,往往会丢失图像中的有价值信息。

事实上,Hinton 本人这样说:

“卷积神经网络中使用的池化操作是一个大错误,然而它之所以能如此有效,正是一个灾难。”

在论文中,Hinton 试图通过逆图形方法提供解决该问题的直觉。对于计算机图形学,图像是通过使用图像中物体的内部表示来构建的。这是通过数组和矩阵来完成的。这种内部表示有助于保留形状、方向和物体之间的相对位置,与图像中的所有其他物体进行比较。软件采用这种内部表示,并通过称为渲染的过程将图像发布到屏幕上。

休·辛顿指出,人脑进行了一种逆向图形处理。我们通过眼睛看到图像,然后大脑会分析这些图像,并构建出图像中不同对象的层次化表示,随后将其与我们曾经见过的模式进行匹配。一个有趣的观察是,人类可以识别图像中的对象,而不论它们的视角如何。

他接着提出,为了进行分类,必须保留图像中不同对象的相对方向和位置(这有助于模拟我们之前讨论过的人类能力)。直观地讲,一旦我们在模型中构建了这些关系,模型就能非常容易地检测到从不同角度看到的对象。让我们设想一下观看泰姬陵(印度的著名建筑)的情况。我们的眼睛可以从不同角度识别泰姬陵。然而,如果你把相同的图像输入到 CNN 中,它可能无法从不同视角识别泰姬陵。这是因为 CNN 不像我们的大脑那样理解 3D 空间。这也是胶囊网络理论如此重要的原因。

这里的一个主要问题是:我们如何将这些层级关系纳入深度神经网络中? 图像中不同对象之间的关系通过一种叫做姿势(pose)的方式来建模,基本上是旋转和平移的结合。这是计算机图形学中的特定概念。在接下来的章节中,我们将讨论这些关系如何在胶囊网络中被建模。

理解胶囊网络

在传统的卷积神经网络(CNN)中,我们定义了不同的滤波器,它们会遍历整个图像。每个滤波器生成的 2D 矩阵会堆叠在一起,形成卷积层的输出。接着,我们执行最大池化操作,以找到活动的变换不变性。这里的变换不变性意味着输出对输入的小变化具有鲁棒性,因为最大池化操作总是选择最大活动。正如前面所提到的,最大池化会导致有价值的信息丢失,并且无法表示图像中不同对象之间的相对方向。

另一方面,胶囊网络以向量形式编码它们检测到的对象的所有信息,而不是通过神经元的标量输出。这些向量具有以下属性:

  • 向量的长度表示图像中对象的概率。

  • 向量的不同元素编码了对象的不同属性。这些属性包括各种实例化参数,如姿势(位置、大小、方向)、色调、厚度等。

通过这种表示方式,如果图像中的物体发生了移动,向量的长度将保持不变。然而,向量表示中不同元素的方向或数值将发生变化。我们再来看之前的泰姬陵的例子。即使我们在图像中移动(或改变方向)泰姬陵,胶囊表示应该仍能在图像中检测到这个物体。

胶囊是如何工作的?

在了解胶囊如何工作的之前,让我们先回顾一下神经元是如何工作的。一个神经元从前一层的神经元接收标量输入,将它们与相应的权重相乘,并将输出求和。这个求和后的输出通过某种非线性函数(如 ReLU)进行处理,输出一个新的标量,然后传递给下一层的神经元。

与此相反,胶囊以向量作为输入,同时也输出一个向量。以下图示展示了计算胶囊输出的过程:

让我们详细看看每一步:

  1. 胶囊 j(在更高层)接收来自下层的向量输入,如u[1]、u[2]、u[3],依此类推。如前所述,每个输入向量编码了在下层检测到的物体的概率以及其方向参数。这些输入向量与权重矩阵W[ij]相乘,后者试图建模下层物体和高层物体之间的关系。在检测泰姬陵的情况下,可以将其视为下层检测到的边缘与上层泰姬陵柱子之间的关系。这个乘法运算的结果是基于下层检测到的物体来预测高层物体(在此为柱子)的向量。因此, 表示基于检测到的垂直边缘,泰姬陵柱子的位置, 则可以表示基于检测到的水平边缘的柱子位置,依此类推。直观地说,如果所有预测的向量都指向同一个物体且具有相似的方向,那么该物体必定存在于图像中:

  1. 接下来,这些预测向量将与标量权重(c[i]s)相乘,这有助于将预测向量路由到上层正确的胶囊。然后,我们将通过此乘法得到的加权向量求和。这一步与传统神经网络中的操作很相似,后者会将标量输入与权重相乘,再将它们提供给更高层次的神经元。在这些情况下,权重是通过反向传播算法来确定的。然而,在胶囊网络中,权重是通过动态路由算法来确定的,我们将在下一节中详细讨论。公式如下:

  1. 在前面的公式中,我们提到了一个新词,叫做压缩。这是胶囊网络中使用的非线性函数。你可以把它看作是传统神经网络中使用的非线性的对应物。从本质上讲,压缩试图将向量缩小到小于单位范数的值,以便将向量的长度解释为概率:

这里,v[j]j层胶囊的输出。

压缩函数在代码中的实现如下:

def squash(vectors, name=None):
 """
 Squashing Function as implemented in the paper
 :parameter vectors: vector input that needs to be squashed
 :parameter name: Name of the tensor on the graph
 :return: a tensor with same shape as vectors but squashed as mentioned in the paper
 """
 with tf.name_scope(name, default_name="squash_op"):
 s_squared_norm = tf.reduce_sum(tf.square(vectors), axis=-2, keepdims=True)
 scale = s_squared_norm / (1\. + s_squared_norm) / tf.sqrt(s_squared_norm + tf.keras.backend.epsilon())
 return scale*vectors

动态路由算法

如前所述,下层胶囊需要决定如何将其输出发送到上层胶囊。这是通过动态路由算法这一新颖的概念来实现的,该算法在论文中被提出(arxiv.org/pdf/1710.09829.pdf)。该算法的关键思想是,下层胶囊将其输出发送到与输入匹配的上层胶囊。

这是通过上一部分提到的权重(c[ij])实现的。这些权重在将来自下层胶囊i的输出作为输入传递给上层胶囊j之前,会先将它们相乘。以下是这些权重的一些特性:

  • c[ij]是非负的,并且由动态路由算法决定

  • 下层胶囊中的权重数量等于上层胶囊的数量

  • 每个下层胶囊i的权重之和为 1

使用以下代码实现迭代路由算法:

def routing(u):
    """
    This function performs the routing algorithm as mentioned in the paper
    :parameter u: Input tensor with [batch_size, num_caps_input_layer=1152, 1, caps_dim_input_layer=8, 1] shape.
                NCAPS_CAPS1: num capsules in the PrimaryCaps layer l
                CAPS_DIM_CAPS2: dimensions of output vectors of Primary caps layer l

    :return: "v_j" vector (tensor) in Digitcaps Layer
             Shape:[batch_size, NCAPS_CAPS1=10, CAPS_DIM_CAPS2=16, 1]
    """
    #local variable b_ij: [batch_size, num_caps_input_layer=1152,
                           num_caps_output_layer=10, 1, 1]
    #num_caps_output_layer: number of capsules in Digicaps layer l+1
    b_ij = tf.zeros([BATCH_SIZE, NCAPS_CAPS1, NCAPS_CAPS2, 1, 1], dtype=np.float32, name="b_ij")

    # Preparing the input Tensor for total number of DigitCaps capsule for multiplication with W
    u = tf.tile(u, [1, 1, b_ij.shape[2].value, 1, 1])   # u => [batch_size, 1152, 10, 8, 1]

    # W: [num_caps_input_layer, num_caps_output_layer, len_u_i, len_v_j] as mentioned in the paper
    W = tf.get_variable('W', shape=(1, u.shape[1].value, b_ij.shape[2].value,    
        u.shape[3].value, CAPS_DIM_CAPS2),dtype=tf.float32,
        initializer=tf.random_normal_initializer(stddev=STDEV))
    W = tf.tile(W, [BATCH_SIZE, 1, 1, 1, 1]) # W => [batch_size, 1152, 10, 8, 16]

    #Computing u_hat (as mentioned in the paper)
    u_hat = tf.matmul(W, u, transpose_a=True)  # [batch_size, 1152, 10, 16, 1]

    # In forward, u_hat_stopped = u_hat;
    # In backward pass, no gradient pass from  u_hat_stopped to u_hat
    u_hat_stopped = tf.stop_gradient(u_hat, name='gradient_stop')

请注意,在前面的代码中,我们将实际的路由函数分成了两部分,以便我们能够专注于动态路由算法部分。该函数的第一部分接收来自下层胶囊的向量u作为输入。首先,它使用权重向量W生成向量 。此外,注意我们定义了一个名为 的临时变量,初始值为零,直到训练开始时。 的值将在算法中更新,并最终存储在c[ij]中。该函数的第二部分实现了实际的迭代路由算法,具体如下:

# Routing Algorithm Begins here
for r in range(ROUTING_ITERATIONS):
    with tf.variable_scope('iterations_' + str(r)):
        c_ij = tf.nn.softmax(b_ij, axis=2) # [batch_size, 1152, 10, 1, 1]

        # At last iteration, use `u_hat` in order to back propagate gradient
        if r == ROUTING_ITERATIONS - 1:
            s_j = tf.multiply(c_ij, u_hat) # [batch_size, 1152, 10, 16, 1]
            # then sum as per paper
            s_j = tf.reduce_sum(s_j, axis=1, keep_dims=True) # [batch_size, 1, 10, 16, 1]

            v_j = squash(s_j) # [batch_size, 1, 10, 16, 1]

        elif r < ROUTING_ITERATIONS - 1:  # No backpropagation in these iterations
            s_j = tf.multiply(c_ij, u_hat_stopped)
            s_j = tf.reduce_sum(s_j, axis=1, keepdims=True)
            v_j = squash(s_j)
            v_j = tf.tile(v_j, [1, u.shape[1].value, 1, 1, 1]) # [batch_size, 1152, 10, 16, 1]

            # Multiplying in last two dimensions: [16, 1]^T x [16, 1] yields [1, 1]
            u_hat_dot_v = tf.matmul(u_hat_stopped, v_j, transpose_a=True) # [batch_size, 1152, 10, 1, 1]

            b_ij = tf.add(b_ij,u_hat_dot_v)
return tf.squeeze(v_j, axis=1) # [batch_size, 10, 16, 1]

首先,我们定义一个ROUTING_ITERATIONS的循环。这个参数由用户定义。Hinton 在他的论文中提到,典型的值3应该足够了。

接下来,我们对  进行 softmax 操作,以计算c[ij]的初始值。请注意,c[ij]不包含在反向传播中,因为这些值只能通过迭代算法获得。因此,在最后一次迭代之前的所有路由迭代都将在  上进行(这有助于停止梯度,正如之前定义的那样)。

对于每次路由迭代,我们对每个上层胶囊j执行以下操作:

我们已经解释了前两个方程。现在让我们试着理解第三个方程。

第三个方程是迭代路由算法的核心。它更新权重 。公式表示新权重值是旧权重的和:来自下层胶囊的预测向量和来自上层胶囊的输出。点积本质上是尝试捕捉输入向量和胶囊输出向量之间的相似性。通过这种方式,来自下层胶囊i的输出只会发送给与其输入一致的上层胶囊j。点积实现了这种一致性。这个算法会重复执行与代码中的ROUTING_ITERATIONS参数相等的次数。

这就是我们对创新性路由算法及其应用的讨论的结束。

CapsNet 用于分类 Fashion MNIST 图像

现在让我们来看一下 CapsNet 在 Fashion MNIST 图像分类中的实现。Zalando,这家电子商务公司,最近发布了一个新的 MNIST 数据集替代品,名为Fashion MNISTgithub.com/zalandoresearch/fashion-mnist)。Fashion MNIST 数据集包含 10 类 28 x 28 灰度图像:

类别名称 标签(数据集中的值)
T-shirt/top 0
Trouser 1
Pullover 2
Dress 3
Coat 4
Sandal 5
Shirt 6
Sneaker 7
Bag 8
Ankle boot 9

以下是数据集中的一些示例图片:

训练集包含 60K 个示例,测试集包含 10K 个示例。

CapsNet 实现

CapsNet 架构由两部分组成,每部分包含三层。前三层是编码器,接下来的三层是解码器:

层编号 层名称 层类型
1 卷积层 编码器
2 初级胶囊层 编码器
3 数字胶囊层 编码器
4 全连接层 1 解码器
5 全连接层 2 解码器
6 全连接层 3 解码器

让我们尝试详细理解这些层。

理解编码器

以下图展示了用于建模的编码器结构。请注意,它显示的是 MNIST 数字图像作为输入,但我们使用的是 Fashion-MNIST 数据集作为模型的输入:

编码器本质上接受一个 28x28 的图像输入,并生成该图像的 16 维表示。如前所述,16 维向量的长度表示图像中是否存在某个物体的概率。向量的各个组成部分表示不同的实例化参数。

专门用于编码器的三层如下:

  • 层 1-卷积层:层 1 是一个标准的卷积层。该层的输入是一个 28x28 的灰度图像,输出是一个 20x20x256 的张量。该层的其他参数如下:
参数名称
过滤器 256
卷积核大小 9
Activation ReLU
Strides 1
  • 层 2-初级胶囊层:层 2 是第一个包含胶囊的层。该层的主要目的是利用第一层卷积层的输出生成更高层次的特征。它有 32 个初级胶囊。它也接受一个 20 x 20 x 256 的张量作为输入。该层中的每个胶囊都将卷积核应用于输入,生成一个 6 x 6 x 8 的张量输出。通过 32 个胶囊,输出现在是一个 6 x 6 x 8 x 32 的张量。

本层中所有胶囊共享的卷积参数如下所示:

参数名称
过滤器 256
Kernel Size 9
激活函数 ReLU
Strides 2

请注意,我们还会对该层的输出进行squash处理。

  • 层 3-DigitCaps 层:此层有 10 个胶囊——每个类标签对应一个胶囊。每个胶囊是一个 16 维的向量。此层的输入是 6x6x32 的 8 维向量(u,如前所述)。每个向量都有自己的权重矩阵,!,它产生!。这些!然后在我们之前描述的路由协议中由一致性算法使用。

请注意,原始论文将此层命名为 DigitCaps 层,因为它使用了 MNIST 数据集。我们继续使用相同的名称来适应 Fashion MNIST 数据集,因为这与原始论文更容易对应。

理解解码器

解码器的结构如下面的图所示:

解码器本质上尝试从每个图像的正确 DigitCaps 胶囊中重建图像。你可以将其视为一个正则化步骤,损失是预测输出与原始标签之间的欧几里得距离。你可以争辩说,在这个应用中不需要重建,因为你只是进行分类。然而,Hinton 在他的原始论文中明确指出,添加重建损失确实提高了模型的准确性。

解码器的结构非常简单,只由三个全连接层组成。三个层的输入和输出形状如下:

输入形状 输出形状
全连接层 4 16 x 10 512
全连接层 5 512 1,024
全连接层 6 1,024 784

然而,在将输入传递给三个全连接层之前,在训练过程中,我们会掩蔽除正确数字胶囊的活动向量外的所有向量。由于在测试过程中我们没有正确的标签,我们会将活动向量的最大范数传递给全连接层。

定义损失函数

胶囊网络的损失函数由两部分组成:

  • 边距损失:边距损失与支持向量机SVM)中使用的完全相同。实际上,我们希望数字胶囊对类k有一个实例化向量,但前提是标签是类k。对于所有其他类别,我们不需要任何实例化参数。对于每个数字胶囊 k,我们定义单独的损失为!

如果图像属于类 k,则!,否则为 0。!是其他两个参数。!在模型初始学习时用于稳定性。总的边距损失是所有数字胶囊损失的总和。

简单来说,对于数字胶囊k(即真实标签),如果我们以大于 0.9 的概率预测出正确的标签,则损失为零;否则,损失为非零。对于所有其他数字胶囊,如果我们预测所有这些类别的概率都小于 0.1,则损失为零;否则,损失为非零。

  • 重建损失:重建损失主要用作模型的正则化器,以便我们能集中精力学习图像的表示,并重现图像。直观地说,这也有助于简化模型实例化参数的学习。它通过计算重建图像和输入图像像素之间的欧几里得距离来生成。模型的总损失如下所示:

总损失 = 边距损失 + 0.0005 重建损失

请注意,重建损失被大大加权,以确保它在训练过程中不会主导边距损失。

训练和测试模型

以下是训练和测试模型的步骤:

  1. 第一步是读取训练和测试数据集。以下是我们必须执行的数据读取步骤:

    • 首先,我们从下载的Fashion MNIST数据集中加载训练/测试图像和标签数据(github.com/zalandoresearch/fashion-mnist)。

    • 然后,我们将图像数据重塑为 28 x 28 x 1 的形状,以供我们的模型使用,并通过 255 进行归一化,保持模型输入在 0 和 1 之间。

    • 我们将训练数据分割为训练集和验证集,每个集分别包含 55,000 张和 5000 张图像。

    • 我们将训练和测试数据集中的目标数组y进行转换,以便我们能获得数据集中 10 个类别的独热表示,这些数据将被输入到模型中。

确保选择大约 10%的数据作为验证集。在这个项目中,我们选择了 5000 张随机图像(占总图像的 8%)作为验证数据集。

前述步骤的代码如下:

def load_data(load_type='train'):
    '''

    :param load_type: train or test depending on the use case
    :return: x (images), y(labels)
    '''
    data_dir = os.path.join('data','fashion-mnist')
    if load_type == 'train':
        image_file = open(os.path.join(data_dir,'train-images-idx3-ubyte'))
        image_data = np.fromfile(file=image_file, dtype=np.uint8)
        x = image_data[16:].reshape((60000, 28, 28, 1)).astype(np.float32)

        label_file = open(os.path.join(data_dir, 'train-labels-idx1-ubyte'))
        label_data = np.fromfile(file=label_file, dtype=np.uint8)
        y = label_data[8:].reshape(60000).astype(np.int32)

        x_train = x[:55000] / 255.
        y_train = y[:55000]
        y_train = (np.arange(N_CLASSES) == y_train[:, None]).astype(np.float32)

        x_valid = x[55000:, ] / 255.
        y_valid = y[55000:]
        y_valid = (np.arange(N_CLASSES) == y_valid[:, None]).astype(np.float32)
        return x_train, y_train, x_valid, y_valid
    elif load_type == 'test':
        image_file = open(os.path.join(data_dir, 't10k-images-idx3-ubyte'))
        image_data = np.fromfile(file=image_file, dtype=np.uint8)
        x_test = image_data[16:].reshape((10000, 28, 28, 1)).astype(np.float)

        label_file = open(os.path.join(data_dir, 't10k-labels-idx1-ubyte'))
        label_data = np.fromfile(file=label_file, dtype=np.uint8)
        y_test = label_data[8:].reshape(10000).astype(np.int32)
        y_test = (np.arange(N_CLASSES) == y_test[:, None]).astype(np.float32)
 return x_test / 255., y_test

请注意,在加载数据集后,我们通过255对图像像素进行归一化,以确保训练的稳定性和更快的收敛。

  1. 通过创建在理解编码器部分中定义的三个神经网络层,来实现编码器:
with tf.variable_scope('Conv1_layer'):
    conv1_layer = tf.layers.conv2d(self.X, name="conv1_layer", **CONV1_LAYER_PARAMS) # [batch_size, 20, 20, 256]

with tf.variable_scope('PrimaryCaps_layer'):
    conv2_layer = tf.layers.conv2d(conv1_layer, name="conv2_layer", **CONV2_LAYER_PARAMS) # [batch_size, 6, 6, 256]

    primary_caps = tf.reshape(conv2_layer, (BATCH_SIZE, NCAPS_CAPS1, CAPS_DIM_CAPS1, 1), name="primary_caps") # [batch_size, 1152, 8, 1]
    primary_caps_output = squash(primary_caps, name="caps1_output")
    # [batch_size, 1152, 8, 1]

# DigitCaps layer, return [batch_size, 10, 16, 1]
with tf.variable_scope('DigitCaps_layer'):
    digitcaps_input = tf.reshape(primary_caps_output, shape=(BATCH_SIZE, NCAPS_CAPS1, 1, CAPS_DIM_CAPS1, 1)) # [batch_size, 1152, 1, 8, 1]
    # [batch_size, 1152, 10, 1, 1]
    self.digitcaps_output = routing(digitcaps_input) # [batch_size, 10, 16, 1]
  1. 接下来,实现解码器层以重建图像,如理解解码器部分所述。以下是参考的重要步骤:
    • 首先,我们计算数字胶囊输出中每个活动向量的范数,用于遮盖目的。我们还会为稳定性添加一个 epsilon 到范数中。

    • 在训练时,我们会遮盖掉数字胶囊输出中的所有活动向量,除了具有正确标签的那个。另一方面,在测试时,我们会遮盖掉数字胶囊输出中的所有活动向量,除了具有最高范数(或预测标签)的那个。我们在解码器中使用tf.cond实现这一分支机制,它在 TensorFlow 图中定义了一个控制流操作。

    • 最后,我们将来自数字胶囊的掩蔽输出展平,并将其展平成一个一维向量,以便可以输入到全连接层。

要了解tf.cond,请参阅www.tensorflow.org/api_docs/python/tf/cond

上述步骤的代码如下:

# Decoder
with tf.variable_scope('Masking'):
    self.v_norm = tf.sqrt(tf.reduce_sum(tf.square(self.digitcaps_output), axis=2, keep_dims=True) + tf.keras.backend.epsilon())

    predicted_class = tf.to_int32(tf.argmax(self.v_norm, axis=1)) #[batch_size, 10,1,1]
    self.y_predicted = tf.reshape(predicted_class, shape=(BATCH_SIZE,))  #[batch_size]
    y_predicted_one_hot = tf.one_hot(self.y_predicted, depth=NCAPS_CAPS2)  #[batch_size,10]  One hot operation

    reconstruction_targets = tf.cond(self.mask_with_labels,  # condition
                              lambda: self.Y,  # if True (Training)
                              lambda: y_predicted_one_hot,  # if False (Test)
                              name="reconstruction_targets")

    digitcaps_output_masked = tf.multiply(tf.squeeze(self.digitcaps_output), tf.expand_dims(reconstruction_targets, -1)) # [batch_size, 10, 16]

    #Flattening as suggested by the paper
    decoder_input = tf.reshape(digitcaps_output_masked, [BATCH_SIZE, -1]) # [batch_size, 160]

with tf.variable_scope('Decoder'):
    fc1 = tf.layers.dense(decoder_input, layer1_size, activation=tf.nn.relu, name="FC1") # [batch_size, 512]
    fc2 = tf.layers.dense(fc1, layer2_size, activation=tf.nn.relu, name="FC2") # [batch_size, 1024]
    self.decoder_output = tf.layers.dense(fc2, output_size, activation=tf.nn.sigmoid, name="FC3") # [batch_size, 784]
  1. 使用定义损失函数部分中提到的公式实现边缘损失,如下所示:
with tf.variable_scope('Margin_Loss'):
    # max(0, m_plus-||v_c||)²
    positive_error = tf.square(tf.maximum(0., 0.9 - self.v_norm)) # [batch_size, 10, 1, 1]
    # max(0, ||v_c||-m_minus)²
    negative_error = tf.square(tf.maximum(0., self.v_norm - 0.1)) # [batch_size, 10, 1, 1]
    # reshape: [batch_size, 10, 1, 1] => [batch_size, 10]
    positive_error = tf.reshape(positive_error, shape=(BATCH_SIZE, -1))
    negative_error = tf.reshape(negative_error, shape=(BATCH_SIZE, -1))

    Loss_vec = self.Y * positive_error + 0.5 * (1- self.Y) * negative_error # [batch_size, 10]
    self.margin_loss = tf.reduce_mean(tf.reduce_sum(Loss_vec, axis=1), name="margin_loss")

  1. 使用定义损失函数部分中提到的公式实现重建损失:
with tf.variable_scope('Reconstruction_Loss'):
    ground_truth = tf.reshape(self.X, shape=(BATCH_SIZE, -1))
    self.reconstruction_loss = tf.reduce_mean(tf.square(self.decoder_output - ground_truth))
  1. 定义优化器为 Adam 优化器,使用默认参数和准确性度量作为常规分类准确率。这些需要在 CapsNet 类中使用以下代码实现:
def define_accuracy(self):
    with tf.variable_scope('Accuracy'):
        correct_predictions = tf.equal(tf.to_int32(tf.argmax(self.Y, axis=1)), self.y_predicted)
        self.accuracy = tf.reduce_mean(tf.cast(correct_predictions, tf.float32))

def define_optimizer(self):
    with tf.variable_scope('Optimizer'):
        optimizer = tf.train.AdamOptimizer()
        self.train_optimizer = optimizer.minimize(self.combined_loss, name="training_optimizer")

要了解更多关于 Adam 优化器的信息,请参阅www.tensorflow.org/api_docs/python/tf/train/AdamOptimizer

  1. 实现对检查点和恢复模型的支持。根据验证集准确率选择最佳模型;我们只在验证集准确率下降的时期进行模型检查点操作,最后,将摘要输出记录到 TensorBoard 进行可视化。我们将模型训练 10 个周期,每个周期的批次大小为 128。记住,你可以调整这些参数来提高模型的准确性:
def train(model):
    global fd_train
    x_train, y_train, x_valid, y_valid = load_data(load_type='train')
    print('Data set Loaded')
    num_batches = int(y_train.shape[0] / BATCH_SIZE)
    if not os.path.exists(CHECKPOINT_PATH_DIR):
        os.makedirs(CHECKPOINT_PATH_DIR)

    with tf.Session() as sess:
        if RESTORE_TRAINING:
            saver = tf.train.Saver()
            ckpt = tf.train.get_checkpoint_state(CHECKPOINT_PATH_DIR)
            saver.restore(sess, ckpt.model_checkpoint_path)
            print('Model Loaded')
            start_epoch = int(str(ckpt.model_checkpoint_path).split('-')[-1])
            train_file, val_file, best_loss_val = load_existing_details()
        else:
            saver = tf.train.Saver(tf.global_variables())
            tf.global_variables_initializer().run()
            print('All variables initialized')
            train_file, val_file = write_progress('train')
            start_epoch = 0
            best_loss_val = np.infty
        print('Training Starts')
        acc_batch_all = loss_batch_all = np.array([])
        train_writer = tf.summary.FileWriter(LOG_DIR, sess.graph)
        for epoch in range(start_epoch, EPOCHS):
            # Shuffle the input data
            x_train, y_train = shuffle_data(x_train, y_train)
            for step in range(num_batches):
                start = step * BATCH_SIZE
                end = (step + 1) * BATCH_SIZE
                global_step = epoch * num_batches + step
                x_batch, y_batch = x_train[start:end], y_train[start:end]
                feed_dict_batch = {model.X: x_batch, model.Y: y_batch, model.mask_with_labels: True}
                if not (step % 100):
                    _, acc_batch, loss_batch, summary_ = sess.run([model.train_optimizer, model.accuracy,
                                                                     model.combined_loss, model.summary_],
                                                                    feed_dict=feed_dict_batch)
                    train_writer.add_summary(summary_, global_step)
                    acc_batch_all = np.append(acc_batch_all, acc_batch)
                    loss_batch_all = np.append(loss_batch_all, loss_batch)
                    mean_acc,mean_loss = np.mean(acc_batch_all),np.mean(loss_batch_all)
                    summary_ = tf.Summary(value=[tf.Summary.Value(tag='Accuracy', simple_value=mean_acc)])
                    train_writer.add_summary(summary_, global_step)
                    summary_ = tf.Summary(value=[tf.Summary.Value(tag='Loss/combined_loss', simple_value=mean_loss)])
                    train_writer.add_summary(summary_, global_step)

                    train_file.write(str(global_step) + ',' + str(mean_acc) + ',' + str(mean_loss) + "\n")
                    train_file.flush()
                    print("  Batch #{0}, Epoch: #{1}, Mean Training loss: {2:.4f}, Mean Training accuracy: {3:.01%}".format(
                        step, (epoch+1), mean_loss, mean_acc))
                    acc_batch_all = loss_batch_all = np.array([])
                else:
                    _, acc_batch, loss_batch = sess.run([model.train_optimizer, model.accuracy, model.combined_loss],
                                                        feed_dict=feed_dict_batch)
                    acc_batch_all = np.append(acc_batch_all, acc_batch)
                    loss_batch_all = np.append(loss_batch_all, loss_batch)

            # Validation metrics after each EPOCH
            acc_val, loss_val = eval_performance(sess, model, x_valid, y_valid)
            val_file.write(str(epoch + 1) + ',' + str(acc_val) + ',' + str(loss_val) + '\n')
            val_file.flush()
            print("\rEpoch: {}  Mean Train Accuracy: {:.4f}% ,Mean Val accuracy: {:.4f}%  Loss: {:.6f}{}".format(
                epoch + 1, mean_acc * 100, acc_val * 100, loss_val,
                " (improved)" if loss_val < best_loss_val else ""))

            # Saving the improved model
            if loss_val < best_loss_val:
                saver.save(sess, CHECKPOINT_PATH_DIR + '/model.tfmodel', global_step=epoch + 1)
                best_loss_val = loss_val
        train_file.close()
        val_file.close()

该模型在验证集和测试集上经过10个周期后达到了几乎99%的准确率,表现相当不错。

重建样本图像

我们还将重建一些样本图像,以查看模型的表现。我们将使用以下图像作为输入:

重建上述图像的代码如下:

def reconstruct_sample(model, n_samples=5):
    x_test, y_test = load_data(load_type='test')
    sample_images, sample_labels = x_test[:BATCH_SIZE], y_test[:BATCH_SIZE]
    saver = tf.train.Saver()
    ckpt = tf.train.get_checkpoint_state(CHECKPOINT_PATH_DIR)
    with tf.Session() as sess:
        saver.restore(sess, ckpt.model_checkpoint_path)
        feed_dict_samples = {model.X: sample_images, model.Y: sample_labels}
        decoder_out, y_predicted = sess.run([model.decoder_output, model.y_predicted],
                                       feed_dict=feed_dict_samples)
    reconstruction(sample_images, sample_labels, decoder_out, y_predicted, n_samples)

用于绘制图像并保存的重建函数如下所示:

def reconstruction(x, y, decoder_output, y_pred, n_samples):
    '''
    This function is used to reconstruct sample images for analysis
    :param x: Images
    :param y: Labels
    :param decoder_output: output from decoder
    :param y_pred: predictions from the model
    :param n_samples: num images
    :return: saves the reconstructed images
    '''

    sample_images = x.reshape(-1, IMG_WIDTH, IMG_HEIGHT)
    decoded_image = decoder_output.reshape([-1, IMG_WIDTH, IMG_WIDTH])

    fig = plt.figure(figsize=(n_samples * 2, 3))
    for i in range(n_samples):
        plt.subplot(1, n_samples, i+ 1)
        plt.imshow(sample_images[i], cmap="binary")
        plt.title("Label:" + IMAGE_LABELS[np.argmax(y[i])])
        plt.axis("off")
    fig.savefig(RESULTS_DIR + '/' + 'input_images.png')
    plt.show()

    fig = plt.figure(figsize=(n_samples * 2, 3))
    for i in range(n_samples):
        plt.subplot(1, n_samples, i + 1)
        plt.imshow(decoded_image[i], cmap="binary")
        plt.title("Prediction:" + IMAGE_LABELS[y_pred[i]])
        plt.axis("off")
    fig.savefig(RESULTS_DIR + '/' + 'decoder_images.png')
    plt.show()

现在,重建的图像如下所示:

如我们所见,标签是完美的,而重建图像虽然不完美,但非常相似。通过更多的超参数调优,我们可以生成更好的重建图像。

胶囊网络的局限性

虽然胶囊网络很棒,并且解决了卷积神经网络的核心问题,但它们仍然有很长的路要走。胶囊网络的一些局限性如下:

  • 该网络尚未在像 ImageNet 这样的超大数据集上进行测试。这对其在大数据集上的表现提出了疑问。

  • 算法运行较慢,主要由于动态路由算法的内部循环。对于大数据集,迭代次数可能相当大。

  • 与 CNN 相比,胶囊网络在实现上确实具有更高的复杂性。

看到深度学习社区如何解决胶囊网络的局限性将会很有趣。

总结

在本章中,我们看到了由 Geoff Hinton(深度学习的奠基人之一)提出的非常流行的神经网络架构 CapsNet。

我们首先理解了卷积神经网络(CNN)在当前形式下的局限性。它们使用最大池化作为一种依赖工具来实现活动的不变性。最大池化有信息丢失的倾向,而且它无法建模图像中不同物体之间的关系。接着,我们讨论了人脑如何检测物体并且具有视角不变性。我们通过类比计算机图形学,理解了如何可能在神经网络中加入姿态信息。

随后,我们了解了胶囊网络的基本构建块——胶囊。我们理解了胶囊与传统神经元的区别,它们接受向量作为输入,并输出一个向量。我们还了解了胶囊中的一种特殊非线性函数——squash函数。

在下一节中,我们了解了一种新颖的动态路由算法,它有助于将低层胶囊的输出路由到高层胶囊。系数  通过多次迭代的路由算法进行学习。算法的关键步骤是通过使用预测向量  和高层胶囊输出向量  的点积来更新系数 

此外,我们为 Fashion MNIST 数据集实现了 CapsNet。我们使用了卷积层,接着是 PrimaryCaps 层和 DigitCaps 层。我们了解了编码器架构,以及如何获取图像的向量表示。然后我们学习了解码器架构,以便从学习到的表示中重建图像。该架构中的损失函数是边际损失(如同支持向量机中的损失)和加权重构损失的组合。为了让模型在训练时能更多地关注边际损失,重构损失被加权降低。

然后我们将模型训练了 10 个周期,批量大小为 128,并在验证集和测试集上达到了超过 99%的准确率。我们重建了一些示例图像以可视化输出,并发现重建结果相当准确。

总结来说,在本章中,我们能够理解并从头开始实现胶囊网络,使用 TensorFlow 并在 Fashion MNIST 数据集上进行训练。

现在,您已经构建了基本的胶囊网络,您可以尝试通过加入多个胶囊层来扩展这个模型,并查看它的表现;您还可以在其他图像数据集上使用这个模型,并观察该算法是否具有可扩展性;或者在没有重构损失的情况下运行,看看是否还能重建输入图像。通过这样做,您将能够对该算法形成较好的直觉。

在下一章中,我们将介绍使用 TensorFlow 进行人脸检测的项目。

第十一章:使用 TensorFlow 进行高质量产品推荐

当你访问亚马逊、Netflix 或其他你喜欢的网站,或使用 Spotify、Pandora 等现代应用时,你会注意到它们会向你推荐不同的商品。这些推荐是通过推荐系统算法生成的。在基于机器学习的推荐系统之前,推荐是通过基于规则的系统生成的。然而,随着机器学习和神经网络的出现,推荐变得更加准确。

在本章中,我们将学习推荐系统。我们将使用 Retailrocket 数据集,通过两种不同的方式实现推荐系统,分别使用 TensorFlow 和 Keras。

本章将涉及以下主题:

  • 推荐系统

  • 基于内容的过滤

  • 协同过滤

  • 混合系统

  • 矩阵分解

  • 介绍 Retailrocket 数据集

  • 探索 Retailrocket 数据集

  • 数据预处理

  • Retailrocket 推荐系统的矩阵分解模型

  • Retailrocket 推荐系统的神经网络模型

推荐系统

机器学习系统的最常见应用之一是向用户推荐他们可能感兴趣的内容。你有没有注意到 Spotify 和 Pandora 如何推荐某种类型的音乐,或者特定的歌曲或电台?你可能也观察到 Netflix 向你推荐电影,如以下截图所示:

亚马逊如何根据你当前浏览的书籍推荐其他书籍,参考以下截图:

这样的系统被称为推荐系统。

推荐系统是学习用户可能感兴趣的项目,然后推荐这些项目供购买、租赁、收听、观看等使用的系统。推荐系统大致可以分为两类:基于内容的过滤和协同过滤。

基于内容的过滤

基于内容的过滤是通过创建内容的详细模型来进行推荐的,这些内容可能是书籍的文本、电影的属性或音乐的信息。内容模型通常表示为向量空间模型。一些常见的将内容转化为向量空间模型的方法包括 TFIDF、词袋模型、Word2Vec、GloVe 和 Item2Vec。

除了内容模型,还通过用户信息创建了用户档案。根据匹配用户档案和内容模型来推荐内容。

基于内容的过滤算法的优点

以下是基于内容的过滤算法的优点:

  • 消除了新物品的冷启动问题: 如果我们拥有足够的用户信息以及新内容的详细信息,那么协同过滤算法中的冷启动问题就不会影响基于内容的算法。推荐可以基于用户档案和内容信息进行。

  • 推荐结果是可解释和透明的: 使用内容表示模型,我们能够解释为什么某些物品被选择作为推荐项。

基于内容的过滤算法的缺点

以下是基于内容的过滤算法的缺点:

  • 基于内容的过滤算法需要详细的物品和内容信息,而这些信息有时并不可用

  • 基于内容的过滤算法容易导致过度专业化

协同过滤

协同过滤算法不需要关于用户或物品的详细信息。它们根据用户与物品的互动(如听过的歌曲、查看的物品、点击的链接、购买的物品或观看的视频)构建模型。通过用户与物品的互动生成的信息可以分为两类:隐式反馈和显式反馈:

  • 显式反馈信息是指用户明确地为物品打分,例如对物品进行 1 到 5 的评分。

  • 隐式反馈信息是通过用户与物品之间的不同互动收集的,例如在 Retailrocket 数据集中,我们将使用的查看、点击、购买等互动。

进一步的协同过滤算法可以分为基于用户和基于物品的两种类型。在基于用户的算法中,重点关注用户之间的互动,以识别相似的用户。然后,系统会推荐其他相似用户购买或查看的物品。在基于物品的算法中,首先根据物品-用户的互动识别相似的物品,然后推荐与当前物品相似的物品。

混合系统

混合系统通过结合两种方法,利用基于内容和协同过滤的优势。混合系统有许多实现方式,例如:

  • 创建基于内容和协同过滤算法的集成,并结合两种算法的推荐结果

  • 通过内容细节和用户信息增强协同过滤

  • 向基于内容的过滤算法添加用户-物品互动模型

鼓励读者进一步探索三种推荐系统。我们将在接下来的章节中,使用 Retailrocket 数据集的示例,探讨如何通过矩阵分解和神经网络构建推荐系统。

矩阵分解

矩阵分解是一种实现推荐系统的流行算法,属于协同过滤算法类别。在此算法中,用户-物品交互被分解为两个低维矩阵。例如,假设我们数据集中的所有访客-物品交互是一个 M x N 的矩阵,记作 A。矩阵分解将矩阵 A 分解成两个分别为 M x k 和 k x N 维度的矩阵,使得这两个矩阵的点积可以逼近矩阵 A。用于寻找低维矩阵的更流行的算法之一是基于奇异值分解SVD)。在下面的示例中,我们将使用 TensorFlow 和 Keras 库来实现矩阵分解。

介绍 Retailrocket 数据集

在本章中,我们将展示如何使用 Retailrocket 数据集实现推荐系统算法。

Retailrocket 数据集可以从 Kaggle 网站下载,网址为www.kaggle.com/retailrocket/ecommerce-dataset

我们使用以下命令下载数据集:

kaggle datasets download -d retailrocket/ecommerce-dataset

下载的文件已移动到~/datasets/kaggle-retailrocket文件夹。你可以根据自己的需要保存在任何文件夹中。

Retailrocket 数据集包含三个文件:

  • events.csv:此文件包含访客-物品交互数据

  • item_properties.csv:此文件包含物品属性

  • category_tree.csv:此文件包含类别树

数据包含来自电商网站的数值,但已被匿名化以确保用户隐私。交互数据代表了 4.5 个月内的交互情况。

访客可以参与三种类别的事件:viewaddtocarttransaction。该数据集包含总共 2,756,101 次交互,包括 2,664,312 次view事件、69,332 次addtocart事件和 22,457 次transaction事件。这些交互来自 1,407,580 个独特的访客。

由于数据包含的是用户-物品交互而不是用户对物品的明确排序,因此它属于隐式反馈信息类别。

探索 Retailrocket 数据集

让我们加载数据集并进行探索,了解更多关于数据的信息。

  1. 设置我们下载数据的文件夹路径:
dsroot = os.path.join(os.path.expanduser('~'),
                      'datasets',
                      'kaggle-retailrocket')
os.listdir(dsroot)
  1. events.csv加载到一个 pandas DataFrame 中:
events = pd.read_csv(os.path.join(dsroot,'events.csv'))
print('Event data\n',events.head())

事件数据包含timestampvisitorideventitemidtransactionid五列,如下所示:

Event data
        timestamp  visitorid event  itemid  transactionid
0  1433221332117     257597  view  355908            NaN
1  1433224214164     992329  view  248676            NaN
2  1433221999827     111016  view  318965            NaN
3  1433221955914     483717  view  253185            NaN
4  1433221337106     951259  view  367447            NaN
  1. 打印唯一的物品、用户和交易:
print('Unique counts:',events.nunique())

我们得到如下输出:

Unique counts: timestamp        2750455
visitorid        1407580
event                  3
itemid            235061
transactionid      17672
dtype: int64
  1. 验证我们之前提到的事件类型:
print('Kind of events:',events.event.unique())

我们看到了之前描述的三种事件:

Kind of events: ['view' 'addtocart' 'transaction']

数据预处理

visitoriditemid字段已为数值型,但我们仍需要将事件转换为数值。

  1. 我们通过以下代码将view事件转换为1addtocart事件转换为2transaction事件转换为3
events.event.replace(to_replace=dict(view=1, 
                                     addtocart=2, 
                                     transaction=3), 
                     inplace=True)
  1. 删除我们不需要的transactionidtimestamp列:
events.drop(['transactionid'],axis=1,inplace=True)
events.drop(['timestamp'],axis=1,inplace=True)
  1. 对数据集进行打乱,以获得用于训练和测试的数据:
events = events.reindex(np.random.permutation(events.index))

数据集也可以通过以下命令进行打乱:

events = events.sample(frac=1).reset_index(drop=True)
  1. 将数据分为 trainvalidtest 集,如下所示:
split_1 = int(0.8 * len(events))
split_2 = int(0.9 * len(events))
train = events[:split_1]
valid = events[split_1:split_2]
test = events[split_2:]
print(train.head())
print(valid.head())
print(test.head())

traintest 数据如下所示:

             timestamp  visitorid  event  itemid
1621867  1431388649092     896963      1  264947
1060311  1440610461477    1102098      1  431592
114317   1433628249991    1241997      1  283584
1658382  1431543289648     198153      1   97879
2173151  1436211020113    1278262      1  218178
             timestamp  visitorid  event  itemid
1903213  1432567070061      85425      1  344338
1722815  1431708672912    1085328      1   59691
1388040  1442124865777    1366284      1  248032
2669880  1438030300131     478634      1  388940
1893864  1432416049191    1052918      1  328647
             timestamp  visitorid  event  itemid
1004940  1440383070554     193171      1   11565
642906   1438664048047     704648      1  262522
902126   1439869996568      10212      1   46971
569976   1435624889084     753933      1   29489
1517206  1430856529370     261457      1  154821

用于 Retailrocket 推荐的矩阵分解模型

现在让我们在 Keras 中创建一个矩阵分解模型:

  1. 将访客和物品的数量存储在一个变量中,如下所示:
n_visitors = events.visitorid.nunique()
n_items = events.itemid.nunique()
  1. 将嵌入层的潜在因子数量设置为 5。你可能想尝试不同的值,以观察对模型训练的影响:
n_latent_factors = 5
  1. 从 Keras 库中导入 Input、Embedding 和 Flatten 层:
from tensorflow.keras.layers import Input, Embedding, Flatten
  1. 从物品开始—创建一个输入层,如下所示:
item_input = Input(shape=[1],name='Items')
  1. 创建一个嵌入表示层,然后将该嵌入层展平,以获得我们之前设置的潜在维度的输出:
item_embed = Embedding(n_items + 1,
                           n_latent_factors, 
                           name='ItemsEmbedding')(item_input)
item_vec = Flatten(name='ItemsFlatten')(item_embed)
  1. 类似地,创建访客的向量空间表示:
visitor_input = Input(shape=[1],name='Visitors')
visitor_embed = Embedding(n_visitors + 1,
                          n_latent_factors,
                          name='VisitorsEmbedding')(visitor_input)
visitor_vec = Flatten(name='VisitorsFlatten')(visitor_embed)
  1. 创建一个点积层,用于表示两个向量空间的点积:
dot_prod = keras.layers.dot([item_vec, visitor_vec],axes=[1,1],
                             name='DotProduct') 
  1. 从输入层构建 Keras 模型,并将点积层作为输出层,然后按如下方式编译:
model = keras.Model([item_input, visitor_input], dot_prod)
model.compile('adam', 'mse')
model.summary()

模型总结如下:

________________________
Layer (type)                    Output Shape         Param #     Connected to                     
================================================================================
Items (InputLayer)              (None, 1)            0                                            
________________________________________________________________________________
Visitors (InputLayer)           (None, 1)            0                                            
________________________________________________________________________________
ItemsEmbedding (Embedding)      (None, 1, 5)         1175310     Items[0][0]                      
________________________________________________________________________________
VisitorsEmbedding (Embedding)   (None, 1, 5)         7037905     Visitors[0][0]                   
________________________________________________________________________________
ItemsFlatten (Flatten)          (None, 5)            0           ItemsEmbedding[0][0]             
________________________________________________________________________________
VisitorsFlatten (Flatten)       (None, 5)            0           VisitorsEmbedding[0][0]          
________________________________________________________________________________
DotProduct (Dot)                (None, 1)            0           ItemsFlatten[0][0]               
                                                                 VisitorsFlatten[0][0]            
================================================================================
Total params: 8,213,215
Trainable params: 8,213,215
Non-trainable params: 0
________________________________________________________________________________

由于模型较为复杂,我们还可以使用以下命令将其图形化:

keras.utils.plot_model(model, 
                       to_file='model.png', 
                       show_shapes=True, 
                       show_layer_names=True)
from IPython import display
display.display(display.Image('model.png'))

你可以通过这个绘制的可视化图清晰地看到各层及输出的大小:

现在让我们训练和评估模型:

model.fit([train.visitorid, train.itemid], train.event, epochs=50)
score = model.evaluate([test.visitorid, test.itemid], test.event)
print('mean squared error:', score)

训练和评估的损失会非常高。我们可以通过使用矩阵分解的高级方法来改进这一点。

现在,让我们构建神经网络模型,以提供相同的推荐。

用于 Retailrocket 推荐的神经网络模型

在这个模型中,我们为用户和物品设置了两个不同的潜在因子变量,但都将它们设置为 5。读者可以尝试使用不同的潜在因子值进行实验:

n_lf_visitor = 5
n_lf_item = 5
  1. 按照我们之前的方法,构建物品和访客的嵌入表示和向量空间表示:
item_input = Input(shape=[1],name='Items')
item_embed = Embedding(n_items + 1,
                           n_lf_visitor, 
                           name='ItemsEmbedding')(item_input)
item_vec = Flatten(name='ItemsFlatten')(item_embed)

visitor_input = Input(shape=[1],name='Visitors')
visitor_embed = Embedding(n_visitors + 1, 
                              n_lf_item,
                              name='VisitorsEmbedding')(visitor_input)
visitor_vec = Flatten(name='VisitorsFlatten')(visitor_embed)
  1. 不再创建点积层,而是将用户和访客的表示进行连接,然后应用全连接层以获得推荐输出:
concat = keras.layers.concatenate([item_vec, visitor_vec], name='Concat')
fc_1 = Dense(80,name='FC-1')(concat)
fc_2 = Dense(40,name='FC-2')(fc_1)
fc_3 = Dense(20,name='FC-3', activation='relu')(fc_2)

output = Dense(1, activation='relu',name='Output')(fc_3)
  1. 按如下方式定义并编译模型:
optimizer = keras.optimizers.Adam(lr=0.001)
model = keras.Model([item_input, visitor_input], output)
model.compile(optimizer=optimizer,loss= 'mse')

让我们看看这个模型的可视化效果:

  1. 训练和评估模型:
model.fit([train.visitorid, train.itemid], train.event, epochs=50)
score = model.evaluate([test.visitorid, test.itemid], test.event)
print('mean squared error:', score)

我们得到了一定准确度,并且误差率非常低:

275611/275611 [==============================] - 4s 14us/step
mean squared error: 0.05709125054560985

就是这样。我们鼓励读者了解更多不同的推荐系统算法,并尝试使用 Retailrocket 或其他公开可用的数据集来实现它们。

总结

在这一章中,我们学习了推荐系统。我们了解了不同种类的推荐系统,如协同过滤、基于内容的过滤和混合系统。我们使用 Retailrocket 数据集创建了两种推荐系统模型,一种是矩阵分解,另一种是使用神经网络。我们看到神经网络模型的准确度相当不错。

在下一章中,我们将学习如何使用分布式 TensorFlow 进行大规模目标检测。

问题

通过练习以下问题来增强理解:

  1. 实现基于文本内容的向量空间模型有哪些算法?

  2. 协同过滤的各种高级算法有哪些?

  3. 如何处理协同过滤模型中的过拟合问题?

  4. 尝试除了本章实现的算法之外的其他算法。

  5. 尝试不同的潜在因子值,分别用于访客和项目。

进一步阅读

你可以通过阅读以下材料来获得更多信息:

  • 以下链接中有推荐系统的教程和文章:recommendation-systems.org

  • 推荐系统手册 第二版 由 Francesco Ricci、Lior Rokach 和 Bracha Shapira 编著,2015 年。

  • 推荐系统:教科书 由 Charu C. Aggarwal 编著,2016 年。

第十二章:使用 TensorFlow 进行大规模目标检测

人工智能AI)领域的最新突破使深度学习成为焦点。如今,越来越多的组织正在采用深度学习技术分析其数据,而这些数据通常是庞大的。因此,将深度学习框架如 TensorFlow 与大数据平台和管道结合变得至关重要。

2017 年 Facebook 的论文讨论了如何使用 256 个 GPU 在 32 台服务器上训练 ImageNet,仅需一小时(research.fb.com/wp-content/uploads/2017/06/imagenet1kin1h5.pdf),以及香港浸会大学最近的论文,他们使用 2,048 个 GPU 在四分钟内训练 ImageNet(arxiv.org/pdf/1807.11205.pdf),这些研究证明了分布式 AI 是一个可行的解决方案。

分布式 AI 的主要思想是将任务划分到不同的处理集群中。已经提出了大量的框架用于分布式 AI。我们可以使用分布式 TensorFlow 或 TensorFlowOnSpark,这两种都是流行的分布式 AI 选择。我们将在本章中了解它们各自的优缺点。

在大规模应用计算密集型深度学习时,可能面临巨大的挑战。使用 TensorFlowOnSpark,我们可以在集群中分布这些计算密集型过程,使我们能够在更大规模上进行计算。

在这一章中,我们将探索 Yahoo 的 TensorFlowOnSpark 框架,用于在 Spark 集群上进行分布式深度学习。然后,我们将在一个大规模图像数据集上应用 TensorFlowOnSpark,并训练网络以检测物体。在这一章中,我们将涵盖以下主题:

  • 对分布式 AI 的需求

  • 对于大数据平台 Apache Spark 的介绍

  • TensorFlowOnSpark – 一种在 Spark 集群上运行 TensorFlow 的 Python 框架

  • 使用 TensorFlowOnSpark 和 Sparkdl API 执行目标检测

对于大数据,Spark 是事实上的首选,因此我们将从 Spark 的介绍开始。然后,我们将探索两种流行的选择:分布式 TensorFlow 和 TensorFlowOnSpark。

本章的代码可以在github.com/PacktPublishing/TensorFlow-Machine-Learning-Projects/tree/master/Chapter12找到。

介绍 Apache Spark

如果你曾经从事过大数据工作,可能已经知道 Apache Spark 是什么,可以跳过这一部分。但如果你不知道,别担心——我们会介绍基本概念。

Spark 是一个强大、快速、可扩展的实时数据分析引擎,用于大规模数据处理。它是一个开源框架,最初由加利福尼亚大学伯克利分校的 AMPLab 开发,约在 2009 年。到 2013 年,AMPLab 将 Spark 贡献给了 Apache 软件基金会,Apache Spark 社区在 2014 年发布了 Spark 1.0。

社区继续定期发布新版本并为项目带来新特性。在写本书时,我们有 Apache Spark 2.4.0 版本以及活跃的 GitHub 社区。它是一个实时数据分析引擎,允许你将程序分布式执行到多台机器上。

Spark 的美妙之处在于它是 可扩展的:它运行在集群管理器之上,允许你在最小修改的情况下使用用 Python(也可以是 Java 或 Scala)编写的脚本。Spark 由多个组件构成。核心部分是 Spark 核心,它负责分发数据处理以及大数据集的映射和归约。上面运行着一些库。以下是 Spark API 中的一些重要组件:

  • 弹性分布式数据集(RDD):RDD 是 Spark API 的基本元素。它是一个容错的元素集合,可以并行操作,这意味着 RDD 中的元素可以被集群中的工作节点同时访问和操作。

  • 转换和操作:在 Spark 的 RDD 上,我们可以执行两种类型的操作,转换和操作。转换以 RDD 作为参数,并返回另一个 RDD。操作以 RDD 作为参数,并返回本地结果。Spark 中的所有转换都是懒加载的,这意味着结果不会立即计算,而是只有当操作需要返回结果时,才会进行计算。

  • 数据框(DataFrames):这些与 pandas 的数据框非常相似。像 pandas 一样,我们可以从多种文件格式(如 JSON、Parquet、Hive 等)中读取数据,并使用单个命令对整个数据框执行操作。它们在集群中分布式运行。Spark 使用一个叫做 Catalyst 的引擎来优化它们的使用。

Spark 使用主/从架构。它有一个主节点/进程和多个工作节点/进程。驱动程序 SparkContext 是 Spark 应用程序的核心。它是 Spark 应用程序的主要入口点和主控,它设置内部服务并与 Spark 执行环境建立连接。下图展示了 Spark 的架构:

到目前为止,我们已经介绍了 Apache Spark。这是一个庞大且广泛的话题,我们建议读者参考 Apache 文档获取更多信息:spark.apache.org/documentation.html

理解分布式 TensorFlow

TensorFlow 还支持分布式计算,允许我们将图拆分并在不同进程上计算。分布式 TensorFlow 工作方式类似于客户端-服务器模型,或者更具体地说,是主节点-工作节点模型。在 TensorFlow 中,我们首先创建一个工作节点集群,其中一个节点是主节点。主节点负责协调任务分配到不同的工作节点。

当你需要在多台机器(或处理器)上工作时,首先要做的事情是定义它们的名称和工作类型,也就是构建一个机器(或处理器)集群。集群中的每台机器都会被分配一个唯一地址(例如,worker0.example.com:2222),并且它们会有一个特定的工作类型,比如type: master(参数服务器),或者是工作节点。稍后,TensorFlow 服务器会将特定的任务分配给每个工作节点。为了创建集群,我们首先需要定义集群规格。这是一个字典,用于映射工作进程和任务类型。以下代码创建了一个名为work的集群,并有两个工作进程:

import tensorflow as tf
cluster = tf.train.ClusterSpec({
   "worker":["worker0.example.com:2222",
           "worker1.example.com:2222"]
})

接下来,我们可以使用Server类并指定任务和任务索引来启动进程。以下代码将在worker1上启动worker任务:

server = tf.train.Server(cluster, job_name = "worker", task_index = 1)

我们需要为集群中的每个工作节点定义一个Server类。这将启动所有工作节点,使我们准备好进行分发。为了将 TensorFlow 操作分配到特定的任务上,我们将使用tf.device来指定哪些任务在哪个工作节点上运行。考虑以下代码,它将在两个工作节点之间分配任务:

import tensorflow as tf

# define Clusters with two workers
cluster = tf.train.ClusterSpec({
    "worker": [
        "localhost:2222",
        "localhost:2223"
         ]})

# define Servers
worker0 = tf.train.Server(cluster, job_name="worker", task_index=0)
worker1 = tf.train.Server(cluster, job_name="worker", task_index=1)

with tf.device("/job:worker/task:1"):
    a = tf.constant(3.0, dtype=tf.float32)
    b = tf.constant(4.0) 
    add_node = tf.add(a,b)

with tf.device("/job:worker/task:0"):
    mul_node = a * b

with tf.Session("grpc://localhost:2222") as sess:
    result = sess.run([add_node, mul_node])
    print(result)

上述代码在同一台机器上创建了两个工作节点。在这种情况下,工作被通过tf.device函数在两个工作节点之间分配。变量在各自的工作节点上创建;TensorFlow 在任务/工作节点之间插入适当的数据传输。

这是通过创建一个GrpcServer来完成的,它通过目标grpc://localhost:2222来创建。这个服务器知道如何通过GrpcChannels与同一任务中的其他任务进行通信。在下面的截图中,你可以看到前述代码的输出:

本章的代码位于仓库中的Chapter12/distributed.py目录下。

这看起来很简单,对吧?但是如果我们想将其扩展到我们的深度学习流水线中呢?

通过分布式 TensorFlow 进行深度学习

任何深度学习算法的核心是随机梯度下降优化器。这使得模型能够学习,同时也让学习过程计算开销大。将计算分发到集群中的不同节点应该能减少训练时间。TensorFlow 允许我们拆分计算图,将模型描述到集群中的不同节点,最后合并结果。

这一点通过主节点、工作节点和参数节点在 TensorFlow 中实现。实际的计算由工作节点执行;计算出的参数由参数节点保存,并与工作节点共享。主节点负责在不同工作节点之间协调工作负载。分布式计算中有两种常用的方法:

  • 同步方法:在这种方法中,工作节点之间分配了小批量数据。每个工作节点都有一个模型副本,并分别计算分配给它的小批量数据的梯度。稍后,梯度在主节点处合并,并同时应用于参数更新。

  • 异步方法:在这种方法中,模型参数的更新是异步应用的。

这两种方法在下图中展示:

现在,让我们看看如何在深度学习管道中集成分布式 TensorFlow。以下代码基于 Medium 上的文章,medium.com/@ntenenz/distributed-tensorflow-2bf94f0205c3

  1. 导入必要的模块。在这里,我们仅导入了必要的模块,以演示将现有深度学习代码转换为分布式 TensorFlow 代码所需的更改:
import sys
import tensorflow as tf
# Add other module libraries you may need
  1. 定义集群。我们将其创建为一个主节点,地址为 192.168.1.3,并且两个工作节点。我们希望将主节点分配到的机器有一个分配给它的 IP 地址,即 192.168.1.3,并且我们指定端口为 2222。你可以根据你机器的地址修改这些设置:
cluster = tf.train.ClusterSpec(
          {'ps':['192.168.1.3:2222'],
           'worker': ['192.168.1.4:2222',
                      '192.168.1.5:2222',
                      '192.168.1.6:2222',
                      '192.168.1.7:2222']
 })
  1. 相同的代码会在每台机器上执行,因此我们需要解析命令行参数:
job = sys.argv[1]
task_idx = sys.argv[2]
  1. 为每个工作节点和主节点创建 TensorFlow 服务器,以便集群中的节点能够进行通信:
server = tf.train.Server(cluster, job_name=job, task_index= int(task_idx))
  1. 确保变量分配在相同的工作设备上。TensorFlow 的 tf.train.replica_device_setter() 函数帮助我们在构造 Operation 对象时自动分配设备。同时,我们希望参数服务器在服务器关闭之前等待。这是通过在参数服务器上使用 server.join() 方法实现的:
if job == 'ps':  
    # Makes the parameter server wait 
    # until the Server shuts down
    server.join()
else:
    # Executes only on worker machines    
    with tf.device(tf.train.replica_device_setter(cluster=cluster, worker_device='/job:worker/task:'+task_idx)):
        #build your model here like you are working on a single machine

    with tf.Session(server.target):
        # Train the model 

你可以通过 GitHub 或 Chapter12/tensorflow_distributed_dl.py 目录访问此脚本。请记住,相同的脚本需要在集群中的每台机器上执行,但命令行参数不同。

相同的脚本现在需要在参数服务器和四个工作节点上执行:

使用以下代码在参数服务器(192.168.1.3:2222)上执行脚本:

python tensorflow_distributed_dl.py ps 0
  1. 使用以下代码在 worker 0192.168.1.4:2222)上执行脚本:
python tensorflow_distributed_dl.py worker 0
  1. 使用以下代码在 worker 1192.168.1.5:2222)上执行脚本:
python tensorflow_distributed_dl.py worker 1
  1. 使用以下代码在 worker 2192.168.1.6:2222)上执行脚本:
python tensorflow_distributed_dl.py worker 2
  1. 使用以下代码在 worker 3192.168.1.6:2222)上执行脚本:
python tensorflow_distributed_dl.py worker 3

分布式 TensorFlow 的主要缺点是我们需要在启动时指定集群中所有节点的 IP 地址和端口。这限制了分布式 TensorFlow 的可扩展性。在下一部分中,您将了解由 Yahoo 构建的 TensorFlowOnSpark API。它提供了一个简化的 API,用于在分布式 Spark 平台上运行深度学习模型。

若要了解更多关于分布式 TensorFlow 的内容,建议您阅读 Google REsearch 团队的论文《TensorFlow: Large Scale Machine Learning on Heterogeneous Distributed Systems》(2012 年 NIPS)(download.tensorflow.org/paper/whitepaper2015.pdf)。

了解 TensorFlowOnSpark

2016 年,Yahoo 开源了 TensorFlowOnSpark,这是一个用于在 Spark 集群上执行基于 TensorFlow 的分布式深度学习的 Python 框架。从那时起,它经历了很多开发变化,是分布式深度学习框架中最活跃的开源项目之一。

TensorFlowOnSparkTFoS)框架允许您在 Spark 程序中运行分布式 TensorFlow 应用。它运行在现有的 Spark 和 Hadoop 集群上。它可以使用现有的 Spark 库,如 SparkSQL 或 MLlib(Spark 的机器学习库)。

TFoS 是自动化的,因此我们无需将节点定义为 PS 节点,也无需将相同的代码上传到集群中的所有节点。只需进行少量修改,我们就可以运行现有的 TensorFlow 代码。它使我们能够以最小的改动扩展现有的 TensorFlow 应用。它支持所有现有的 TensorFlow 功能,如同步/异步训练、数据并行和 TensorBoard。基本上,它是 TensorFlow 代码的 PySpark 封装。它通过 Spark 执行器启动分布式 TensorFlow 集群。为了支持 TensorFlow 的数据摄取,它添加了feed_dictqueue_runner,允许直接从 TensorFlow 访问 HDFS。

理解 TensorFlowOnSpark 的架构

以下图示展示了 TFoS 的架构。我们可以看到,TFoS 在张量通信中不涉及 Spark 驱动程序,提供与独立 TensorFlow 集群相同的可扩展性:

TFoS 提供了两种输入模式,用于训练和推理时获取数据:

  • Spark RDD:Spark RDD 数据被传递到每个 Spark 执行器。执行器将数据通过feed_dict传递给 TensorFlow 图。然而,在这种模式下,TensorFlow 工作节点的失败对 Spark 是隐藏的。

  • TensorFlow QueueRunners:在这里,TensorFlow 工作节点在前台运行。TFoS 利用 TensorFlow 的文件读取器和 QueueRunners,直接从 HDFS 文件读取数据。TensorFlow 工作节点的失败会被视为 Spark 任务,并通过检查点进行恢复。

深入探讨 TFoS API

使用 TFoS 可以分为三个基本步骤:

  1. 启动 TensorFlow 集群。我们可以使用TFCluster.run来启动集群:
cluster = TFCluster.run(sc, map_fn, args, num_executors, num_ps, tensorboard, input_mode)
  1. 将数据输入 TensorFlow 应用程序。数据用于训练和推理。为了训练,我们使用train方法:
cluster.train(dataRDD, num_epochs)

我们通过cluster.inference(dataRDD)来执行推理。

  1. 最后,通过cluster.shutdown()关闭 TensorFlow 集群。

我们可以修改任何 TensorFlow 程序以与 TFoS 一起使用。在接下来的部分中,我们将介绍如何使用 TFoS 训练一个模型来识别手写数字。

使用 TFoS 进行手写数字识别

在本节中,我们将介绍如何将 TensorFlow 代码转换为在 TFoS 上运行。为此,首先,我们需要在 Amazon AWS 上构建一个 EC2 集群。一个简单的方法是使用 Flintrock,这是一个从本地机器启动 Apache Spark 集群的 CLI 工具。

以下是完成本节所需的先决条件:

  • Hadoop

  • PySpark

  • Flintrock

  • Python

  • TensorFlow

  • TensorFlowOnSpark

现在,让我们看看如何实现这一点。我们使用的是 MNIST 数据集(yann.lecun.com/exdb/mnist/)。以下代码来自 TensorFlowOnSpark 的 GitHub 仓库。该仓库包含文档链接和更多示例(github.com/yahoo/TensorFlowOnSpark):

  1. main(argv, ctx)函数中定义模型架构和训练,其中argv参数包含命令行传递的参数,而ctx包含节点元数据,如jobtask_idxcnn_model_fn模型函数是定义的 CNN 模型:
def main(args, ctx):
    # Load training and eval data
    mnist = tf.contrib.learn.datasets.mnist.read_data_sets(args.data_dir)
    train_data = mnist.train.images # Returns np.array
    train_labels = np.asarray(mnist.train.labels, dtype=np.int32)
    eval_data = mnist.test.images # Returns np.array
    eval_labels = np.asarray(mnist.test.labels, dtype=np.int32)

    # Create the Estimator
    mnist_classifier = tf.estimator.Estimator(model_fn=cnn_model_fn, model_dir=args.model)

    # Set up logging for predictions
    # Log the values in the "Softmax" tensor with label "probabilities"

    tensors_to_log = {"probabilities": "softmax_tensor"}
    logging_hook = tf.train.LoggingTensorHook( tensors=tensors_to_log, every_n_iter=50)

    # Train the model
    train_input_fn = tf.estimator.inputs.numpy_input_fn(
         x={"x": train_data}, y=train_labels, 
         batch_size=args.batch_size, num_epochs=None, 
         shuffle=True)

      eval_input_fn = tf.estimator.inputs.numpy_input_fn(
         x={"x": eval_data},
         y=eval_labels,
         num_epochs=1,
         shuffle=False)

    #Using tf.estimator.train_and_evaluate
    train_spec = tf.estimator.TrainSpec(
        input_fn=train_input_fn, 
        max_steps=args.steps, 
        hooks=[logging_hook])
    eval_spec = tf.estimator.EvalSpec(
        input_fn=eval_input_fn)
    tf.estimator.train_and_evaluate(
        mnist_classifier, train_spec, eval_spec)

  1. if __name__=="__main__"块中,添加以下导入:
from pyspark.context import SparkContext
from pyspark.conf import SparkConf
from tensorflowonspark import TFCluster
import argparse
  1. 启动 Spark Driver 并初始化 TensorFlowOnSpark 集群:
sc = SparkContext(conf=SparkConf()
        .setAppName("mnist_spark"))
executors = sc._conf.get("spark.executor.instances")
num_executors = int(executors) if executors is not None else 1
  1. 解析参数:
parser = argparse.ArgumentParser()
parser.add_argument("--batch_size", 
            help="number of records per batch", 
            type=int, default=100)
parser.add_argument("--cluster_size", 
            help="number of nodes in the cluster", 
            type=int, default=num_executors)
parser.add_argument("--data_dir", 
            help="path to MNIST data", 
            default="MNIST-data")
parser.add_argument("--model", 
            help="path to save model/checkpoint", 
            default="mnist_model")
parser.add_argument("--num_ps", 
            help="number of PS nodes in cluster", 
            type=int, default=1)
parser.add_argument("--steps", 
            help="maximum number of steps", 
            type=int, default=1000)
parser.add_argument("--tensorboard", 
            help="launch tensorboard process", 
            action="store_true")

args = parser.parse_args()
  1. 使用TFCluster.run来管理集群:
cluster = TFCluster.run(sc, main, args, 
        args.cluster_size, args.num_ps, 
        tensorboard=args.tensorboard, 
        input_mode=TFCluster.InputMode.TENSORFLOW, 
        log_dir=args.model, master_node='master')
  1. 训练完成后,关闭集群:
cluster.shutdown()

完整代码可以在 GitHub 仓库的Chapter12/mnist_TFoS.py目录中找到。

要在 EC2 集群上执行代码,您需要使用spark-submit将其提交到 Spark 集群:

${SPARK_HOME}/bin/spark-submit \
--master ${MASTER} \
--conf spark.cores.max=${TOTAL_CORES} \
--conf spark.task.cpus=${CORES_PER_WORKER} \
--conf spark.task.maxFailures=1 \
--conf spark.executorEnv.JAVA_HOME="$JAVA_HOME" \
${TFoS_HOME}/examples/mnist/estimator/mnist_TFoS.py \
--cluster_size ${SPARK_WORKER_INSTANCES} \
--model ${TFoS_HOME}/mnist_model

该模型在 EC2 集群上用两台工作节点训练了 6.6 分钟:

我们可以使用 TensorBoard 来可视化模型架构。一旦代码成功运行,事件文件会被创建,并且可以在 TensorBoard 中查看。

当我们可视化损失时,可以看到随着网络学习,损失逐渐减少:

该模型在测试数据集上只用了 1,000 步就获得了 75%的准确率,且使用了一个非常基础的 CNN 模型。我们可以通过使用更好的模型架构和调整超参数进一步优化结果。

使用 TensorFlowOnSpark 和 Sparkdl 进行物体检测

Apache Spark 提供了一个高级 API Sparkdl,用于在 Python 中进行可扩展的深度学习。在本节中,我们将使用 Sparkdl API。在本节中,您将学习如何在预训练的 Inception v3 模型基础上构建一个模型,用于检测汽车和公交车。使用预训练模型的技术称为 迁移学习

迁移学习

人类的学习是一个持续的过程——我们今天学到的知识是建立在过去学习的基础上的。例如,如果你会骑自行车,你可以将相同的知识扩展到骑摩托车或开汽车。驾驶规则是一样的——唯一不同的是控制面板和执行器。然而,在深度学习中,我们通常是从头开始。是否可以利用模型在一个领域解决问题时获得的知识,来解决另一个相关领域中的问题呢?

是的,确实可以,这就是迁移学习。虽然该领域仍在进行大量研究,但在计算机视觉领域,迁移学习的应用已取得了巨大的成功。这是因为对于计算机视觉任务,卷积神经网络CNNs)被优先使用,因为它们擅长从图像中提取特征(例如,较低层次的特征如线条、圆圈和方块,以及较高层次的抽象特征如耳朵和鼻子)。因此,在学习一种类型的图像数据集时,卷积层提取的特征可以在其他相似领域的图像中重复使用。这有助于减少训练时间。

在本节中,我们将使用 Inception v3(arxiv.org/pdf/1512.00567v1.pdf),一个经过训练的最先进的 CNN,使用的是 ImageNet 数据集。ImageNet(image-net.org/)包含超过 1400 万张标注的高分辨率手工标注图像,这些图像被分类为 22,000 个类别。Inception v3 是在其子集上训练的,包含大约 130 万张图像和 1,000 个类别。

在迁移学习方法中,您保留特征提取的 CNN 层,但将分类器层替换为新的分类器。然后在新的图像上训练这个新的分类器。一般有两种方法:要么只训练新的分类器,要么微调整个网络。在第一种情况下,我们通过将新数据集输入 CNN 层来提取 瓶颈特征。然后将提取的瓶颈特征用于训练最终的分类器。在第二种情况下,我们在训练数据集上训练整个网络,原始的 CNN 以及新的分类器。

理解 Sparkdl 接口

要访问深度学习管道中的 Spark 功能,我们需要使用 Spark 驱动程序。自 Spark 2.0.0 起,我们有一个单一的入口点,使用 SparkSession。最简单的方法是使用 builder

SparkSession.builder().getOrCreate()

这样我们就可以获取现有的会话或创建一个新会话。在实例化时,我们可以使用 .config().master().appName() 方法来设置配置选项、设置 Spark 主节点和设置应用程序名称。

为了读取和处理图片,Sparkdl 提供了 ImageSchema 类。在其众多方法中,我们将使用 readImages 方法读取图片目录。它返回一个包含单个列 image 的 Spark DataFrame,其中存储了图片。

我们可以使用转换来添加或删除 Spark DataFrame 中的列/行。本节中的示例代码使用 withColumn 转换添加了一个名为 label 的列,并为我们的数据集分配了标签类。就像使用 pandas DataFrame 一样,我们可以通过 show() 方法查看 Spark DataFrame 的行。Spark DataFrame 还可以被拆分或组合在一起。

Sparkdl API 提供了方法来实现快速的迁移学习。它提供了 DeepImageFeaturizer 类,自动剥离预训练模型中的分类器层,并使用预训练 CNN 层的特征(瓶颈特征)作为新分类器的输入。

使用 Sparkdl 的一个优势是我们可以通过相同的 SparkSession 实例访问所有 Spark API,甚至包括其机器学习 API MLlib。通过 MLlib,我们可以轻松地将多个算法结合成一个管道。Spark 机器学习 API MLlib 还提供对各种分类和回归方法的支持。

构建一个物体检测模型

现在我们将使用 TFoS 和 Sparkdl 编写一些代码。数据集由从 Google 图片搜索中整理出来的公交车和汽车图片组成。目标是训练一个模型,使其能够区分汽车和公交车。以下是您需要的先决条件,以确保此代码能够运行:

  • PySpark

  • Python

  • TensorFlow

  • TensorFlowOnSpark

  • Pillow

  • Keras

  • TensorFrames

  • Wrapt

  • pandas

  • FindSpark

  • py4j

首先,让我们查看一下我们的数据集。Inception v3 在包含 1,000 类别的 ImageNet 数据上进行了训练,其中也包含了各种车辆的图片。我们有 49 张公交车的图片和 41 张汽车的图片。这里,您可以看到数据集中的一些示例图片:

现在,让我们构建代码:

  1. 这次,我们不使用 spark-submit。相反,我们像运行任何标准的 Python 代码一样运行代码。因此,我们将在代码中定义 Spark 驱动程序的位置和 Spark 深度学习包,并使用 PySpark 的 SparkSession 构建器创建一个 Spark 会话。这里需要记住的一点是分配给堆的内存:Spark 执行器和 Spark 驱动程序。这个值应该基于您机器的规格:
import findspark
findspark.init('/home/ubuntu/spark-2.4.0-bin-hadoop2.7')

import os
SUBMIT_ARGS = "--packages databricks:spark-deep-learning:1.3.0-spark2.4-s_2.11 pyspark-shell"
os.environ["PYSPARK_SUBMIT_ARGS"] = SUBMIT_ARGS

from pyspark.sql import SparkSession
spark = SparkSession.builder \
    .appName("ImageClassification") \
    .config("spark.executor.memory", "70g") \
    .config("spark.driver.memory", "50g") \
    .config("spark.memory.offHeap.enabled",True) \
    .config("spark.memory.offHeap.size","16g") \
    .getOrCreate()
  1. 图片通过 PySpark 的 ImageSchema 类加载到 Spark DataFrame 中。公交车和汽车的图片分别加载到不同的 Spark DataFrame 中:
import pyspark.sql.functions as f
import sparkdl as dl
from pyspark.ml.image import ImageSchema

dfbuses = ImageSchema.readImages('buses/').withColumn('label', f.lit(0))
dfcars = ImageSchema.readImages('cars/').withColumn('label', f.lit(1))
  1. 你可以在这里看到 Spark DataFrame 的前五行:
dfbuses.show(5)
dfcars.show(5)

前面代码的输出结果如下:

  1. 我们将数据集划分为训练集和测试集,比例为 60%的训练集和 40%的测试集。请记住,这些值是随机的,你可以根据需要进行调整:
trainDFbuses, testDFbuses = dfbuses.randomSplit([0.60,0.40], seed = 123)
trainDFcars, testDFcars = dfcars.randomSplit([0.60,0.40], seed = 122)
  1. 公交车和汽车的训练数据集已合并。测试数据集也进行了相同的处理:
trainDF = trainDFbuses.unionAll(trainDFcars)
testDF = testDFbuses.unionAll(testDFcars)
  1. 我们使用 Sparkdl API 获取预训练的 Inception v3 模型,并在 Inception 的 CNN 层上添加了一个逻辑回归器。现在,我们将在我们的数据集上训练这个模型:
from pyspark.ml.classification import LogisticRegression
from pyspark.ml import Pipeline
vectorizer = dl.DeepImageFeaturizer(inputCol="image",
         outputCol="features", 
        modelName="InceptionV3")

logreg = LogisticRegression(maxIter=30, labelCol="label")
pipeline = Pipeline(stages=[vectorizer, logreg])
pipeline_model = pipeline.fit(trainDF)
  1. 让我们看看训练好的模型在测试数据集上的表现。我们使用完美的混淆矩阵来进行评估:
predictDF = pipeline_model.transform(testDF)
predictDF.select('prediction', 'label').show(n = testDF.toPandas().shape[0], truncate=False)
predictDF.crosstab('prediction', 'label').show()

前面代码的输出结果如下:

  1. 对于测试数据集,模型的准确率达到了 100%:
from pyspark.ml.evaluation import MulticlassClassificationEvaluator
scoring = predictDF.select("prediction", "label")
accuracy_score = MulticlassClassificationEvaluator(metricName="accuracy")
rate = accuracy_score.evaluate(scoring)*100
print("accuracy: {}%" .format(round(rate,2)))

我们的模型表现如此出色,是因为我们作为迁移学习基础模型使用的 Inception v3 模型已经在大量的车辆图像上进行了训练。然而,需要提醒的是,100%的准确率并不意味着这是最好的模型,只是说明它在当前测试图像上表现良好。

Sparkdl 是由 DataBricks 开发的,属于深度学习管道的一部分。它提供了用于 Python 中基于 Apache Spark 的可扩展深度学习的高级 API。你可以在这里了解更多关于它的功能及如何使用:github.com/databricks/spark-deep-learning

总结

深度学习模型在训练数据集较大时(大数据)能提供更好的性能。训练大数据模型在计算上开销很大。这个问题可以通过分治法来解决:我们将大量的计算任务分配给集群中的多台机器,换句话说,就是分布式人工智能。

实现这一目标的一种方法是使用 Google 的分布式 TensorFlow,这是一个帮助将模型训练分配到集群中不同工作机器的 API。你需要指定每台工作机器和参数服务器的地址。这使得模型扩展变得困难且繁琐。

这个问题可以通过使用 TensorFlowOnSpark API 来解决。通过对已有的 TensorFlow 代码进行最小的修改,我们可以使其在集群上运行。Spark 框架处理执行器机器和主节点之间的分配,避免了用户关注细节,同时提供了更好的可扩展性。

在本章中,使用了 TensorFlowOnSpark API 来训练一个模型,以识别手写数字。这解决了可扩展性的问题,但我们仍然需要处理数据,以确保它以正确的格式提供给训练。除非你对 Spark 基础设施,尤其是 Hadoop,十分熟悉,否则这可能是一个困难的任务。

为了降低难度,我们可以利用另一个 API,Sparkdl,它提供了基于 Spark 的完整深度学习训练管道,使用 Spark DataFrames 进行训练。最后,本章使用了 Sparkdl API 进行目标检测。一个模型在预训练的 Inception v3 模型基础上构建,用于分类公共汽车和汽车的图像。

在下一章中,你将学习如何使用 RNN 生成书籍脚本。谁知道呢——它也许会赢得布克奖!

第十三章:使用 LSTM 生成书籍脚本

自然语言生成NLG),作为人工智能的一个子领域,是从各种数据输入中生成可读的文本的自然语言处理任务。这是一个活跃的研究领域,近年来已获得广泛关注。

机器生成自然语言的能力可以有广泛的应用,包括手机中的文本自动补全功能、文档摘要生成,甚至是为喜剧创作新剧本。Google 的智能回复也使用了一种类似的技术来在你写电子邮件时提供回复建议。

在本章中,我们将研究一个 NLG 任务——从另一本名为《Mastering PostgreSQL 10》的 Packt 书籍中生成书籍脚本。我们从这本书中挑选了近 100 页,移除了所有图表、表格和 SQL 代码。数据量相当大,足以让神经网络学习数据集的细微差别。

我们将通过以下主题学习如何使用强化学习神经网络生成书籍脚本:

  • 循环神经网络和 LSTM 简介

  • 书籍脚本数据集的描述

  • 使用 LSTM 建模并生成新的书籍脚本

理解循环神经网络

循环神经网络RNNs)已成为任何涉及序列数据的任务中极为流行的选择。RNNs 的核心理念是利用数据中存在的序列信息。通常情况下,每个神经网络假设所有输入彼此独立。然而,如果我们要预测序列中的下一个词或时间序列中的下一个点,就必须使用基于之前使用的词或时间序列中的历史点的信息。

一种理解循环神经网络(RNNs)概念的方法是,它们具有一个内存,能够存储关于序列中历史数据的信息。理论上,RNNs 可以记住任意长序列的历史,但在实际应用中,它们在需要保留超过几个步骤的历史信息的任务中表现不佳。

RNN 的典型结构如下:

在前面的图示中,Xt 是不同时间步长的序列值。RNNs 被称为循环,因为它们对序列中的每个元素应用相同的操作,输出依赖于前一步的结果。可以清楚地观察到单元之间的连接,这些连接帮助将信息从前一步传递到下一步。

如前所述,RNNs 并不擅长捕捉长期依赖关系。RNNs 有不同的变种,其中一些如下:

  • 长短期记忆LSTMs

  • 门控循环单元GRU

  • 盯孔 LSTM(Peephole LSTMs)

与传统的 RNN 相比,LSTM 在捕捉长期依赖方面表现更好。LSTM 在诸如单词/句子预测、图像字幕生成,甚至是需要长期依赖的时间序列数据预测等任务中变得非常流行。以下是使用 LSTM 的一些优点:

  • 擅长建模涉及长期依赖的任务

  • 不同时间步之间的权重共享大大减少了模型中的参数数量

  • 比传统的 RNN 更少受到梯度消失和梯度爆炸问题的困扰

以下是使用 LSTM 的一些缺点:

  • LSTM 对数据有较高需求。通常需要大量的训练数据才能产生有意义的结果。

  • 训练速度比传统神经网络慢。

  • 存在计算效率更高的 RNN 变种,如 GRU,它们能实现与 LSTM 相似的性能。

本章讨论的内容不涉及其他类型的 RNN。如果您感兴趣,可以参考《深度学习》书中的序列建模章节(www.deeplearningbook.org/contents/rnn.html)。

数据预处理

如前所述,本项目使用的数据集来自一本流行的 Packt 书籍,书名为《Mastering PostgreSQL 10》,作者是 Hans-Jürgen Schönig (www.cybertec-postgresql.com)。我们考虑了书籍前 100 页的文本,排除了所有图形、表格和 SQL 代码。清理后的数据集与代码一起存储在一个文本文件中。数据集包含近 44,000 个单词,足以用来训练模型。以下是脚本中的几行:

"PostgreSQL 概述

PostgreSQL 是世界上最先进的开源数据库系统之一,具有许多功能,广泛受到开发者和系统管理员的使用。从 PostgreSQL 10 开始,许多新功能已被添加到 PostgreSQL 中,这些新功能极大地促进了这一卓越开源产品的成功。在本书中,将详细讲解和讨论许多这些酷炫的功能。

在本章中,您将了解 PostgreSQL 以及 PostgreSQL 10.0 及更高版本中的一些酷炫的新功能。所有相关的新功能将详细讲解。鉴于代码修改的数量以及 PostgreSQL 项目的庞大规模,这个功能列表显然远非完整,因此我尝试专注于最重要、最相关的大多数人都会用到的方面。

本章中概述的功能将分为以下几个类别:数据库管理

与 SQL 和开发者相关的备份、恢复和复制、性能相关主题

PostgreSQL 10.0 中的新功能。

PostgreSQL 10.0 于 2017 年底发布,是第一个采用 PostgreSQL 社区引入的新编号方案的版本。从现在开始,主要版本的发布方式将发生变化,因此,PostgreSQL 之后的下一个主要版本

10.0 不会是 10.1,而是 PostgreSQL 11。版本 10.1 和 10.2 只是服务版本,只会包含错误修复。

为了预处理数据并为 LSTM 模型做准备,请按照以下步骤进行:

  1. 标记化标点符号:在预处理过程中,我们将拆分标准设定为使用空格分隔的单词。然而,在这种情况下,神经网络将难以区分像“Hello”和“Hello!”这样的词。由于这个限制,需要在数据集中对标点符号进行标记化。例如,! 将被映射为 _Sym_Exclamation_。在代码中,我们实现了一个名为 define_tokens 的函数。它用于创建一个字典,在这个字典中,每个标点符号是键,对应的标记是值。这个示例中,我们将为以下符号创建标记:
    • 句号 ( . )

    • 逗号 ( , )

    • 引号 ( " )

    • 分号 ( ; )

    • 感叹号 ( ! )

    • 问号 ( ? )

    • 左括号 ( ( )

    • 右括号 ( ) )

    • 连字符 ( -- )

    • 返回 ( \n )

避免使用数据集中可能出现的词语。例如,? 被替换为 _Sym_Question_,这在数据集中不是一个单词。

  1. 转为小写并分割:我们必须将文本中的所有大写字母转换为小写字母,以便神经网络能够学习到“Hello”和“hello”实际上是相同的两个词。由于神经网络的基本输入单元是单词,下一步就是将文本中的句子拆分为单词。

  2. 映射创建:神经网络不能直接接受文本作为输入,因此我们需要将这些单词映射到索引/ID。为此,我们必须创建两个字典,如下所示:

    • Vocab_to_int:将文本中的每个单词映射到其唯一的 ID

    • Int_to_vocab:反向字典,将 ID 映射到对应的单词

定义模型

在使用预处理数据训练模型之前,我们先了解一下这个问题的模型定义。在代码中,我们在 model.py 文件中定义了一个模型类。该类包含四个主要部分,具体如下:

  • 输入:我们在模型中定义了 TensorFlow 的占位符,用于输入(X)和目标(Y)。

  • 网络定义:该模型的网络有四个组件,具体如下:

    • 初始化 LSTM 单元:为此,我们首先将两层 LSTM 堆叠在一起。然后,我们将 LSTM 的大小设置为代码中定义的 RNN_SIZE 参数。接着,RNN 被初始化为零状态。

    • 词嵌入:我们使用词嵌入来对文本中的词进行编码,而不是使用 one-hot 编码。这样做的主要目的是减少训练集的维度,从而帮助神经网络更快地学习。我们从均匀分布中为词汇表中的每个词生成嵌入,并使用 TensorFlow 的embedding_lookup函数来获取输入数据的嵌入序列。

    • 构建 LSTM:为了获得 LSTM 的最终状态,我们使用 TensorFlow 的tf.nn.dynamic_rnn函数,并传入初始单元和输入数据的嵌入。

    • 概率生成:在获得 LSTM 的最终状态和输出后,我们将其通过一个全连接层生成预测的 logits。我们使用softmax函数将这些 logits 转换为概率估计。代码如下:

  • 序列损失:我们必须定义损失函数,在这个情况下是序列损失。这实际上只是对一系列 logits 进行加权交叉熵损失计算。我们在批次和时间上对观测值进行等权重处理。

  • 优化器:我们将使用 Adam 优化器,并保持其默认参数。我们还将裁剪梯度,确保其在-1 到 1 的范围内。梯度裁剪是递归神经网络中的常见现象。当梯度在时间上反向传播时,如果它们不断地与小于 1 的数相乘,梯度可能会消失;或者如果与大于 1 的数相乘,梯度可能会爆炸。梯度裁剪通过将梯度限制在-1 到 1 之间,帮助解决这两个问题。

训练模型

在理解训练循环的实现之前,让我们仔细看看如何生成数据批次。

众所周知,神经网络使用批次来加速模型训练,并减少内存消耗。批次是原始数据集的样本,用于网络的正向传播和反向传播。正向传播指的是将输入与网络中不同层的权重相乘并获得最终输出的过程。反向传播则是基于正向传播输出的损失,更新神经网络中的权重。

在这个模型中,由于我们是在根据一组前置词来预测下一个词组以生成电视脚本,目标基本上是原始训练数据集中下一个词(根据序列长度)的几个词。我们来看一个例子,假设训练数据集包含如下内容:

The quick brown fox jumps over the lazy dog

如果使用的序列长度(处理的词数)是 4,那么以下内容成立:

  • X 是每四个词的序列,例如,[The quick brown fox, quick brown fox jumps …..]。

  • Y 是每四个词的序列,跳过第一个词,例如,[quick brown fox jumps, brown fox jumps over …]。

定义并训练一个文本生成模型

  1. 使用load_data函数加载保存的文本数据以进行预处理:
 def load_data():
 """
 Loading Data
 """
 input_file = os.path.join(TEXT_SAVE_DIR)
 with open(input_file, "r") as f:
 data = f.read()

return data
  1. 实现define_tokens,如本章数据预处理部分所定义。这将帮助我们创建一个关键字及其相应 tokens 的字典:
 def define_tokens():
 """
 Generate a dict to turn punctuation into a token. Note that Sym before each text denotes Symbol
 :return: Tokenize dictionary where the key is the punctuation and the value is the token
 """
 dict = {'.':'_Sym_Period_',
 ',':'_Sym_Comma_',
 '"':'_Sym_Quote_',
 ';':'_Sym_Semicolon_',
 '!':'_Sym_Exclamation_',
 '?':'_Sym_Question_',
 '(':'_Sym_Left_Parentheses_',
 ')':'_Sym_Right_Parentheses_',
 '--':'_Sym_Dash_',
 '\n':'_Sym_Return_',
 }
 return dict

我们创建的字典将用于用相应的 tokens 和分隔符(此处为空格)替换数据集中的标点符号。例如,Hello!将被替换为Hello _Sym_Exclamation_

注意,Hello和 token 之间有一个空格。这将帮助 LSTM 模型将每个标点符号当作独立的单词来处理。

  1. 使用Vocab_to_intint_to_vocab字典帮助将单词映射到索引/ID。我们这样做是因为神经网络不接受文本作为输入:
 def create_map(input_text):
 """
 Map words in vocab to int and vice versa for easy lookup
 :param input_text: TV Script data split into words
 :return: A tuple of dicts (vocab_to_int, int_to_vocab)
 """
 vocab = set(input_text)
 vocab_to_int = {c: i for i, c in enumerate(vocab)}
 int_to_vocab = dict(enumerate(vocab))
 return vocab_to_int, int_to_vocab
  1. 将前面所有步骤结合起来,创建一个函数来预处理我们可用的数据:
def preprocess_and_save_data():
 """
 Preprocessing the TV Scripts Dataset
 """
 generate_text_data_from_csv()
 text = load_data()
 text= text[14:] # Ignoring the STARTraw_text part of the dataset
 token_dict = define_tokens()
 for key, token in token_dict.items():
 text = text.replace(key, ' {} '.format(token))

text = text.lower()
 text = text.split()

vocab_to_int, int_to_vocab = create_map(text)
 int_text = [vocab_to_int[word] for word in text]
 pickle.dump((int_text, vocab_to_int, int_to_vocab, token_dict), open('processed_text.p', 'wb'))

然后我们将为映射字典生成整数文本,并将预处理后的数据和相关字典存储到pickle文件中。

  1. 为了定义我们的模型,我们将在model.py文件中创建一个模型类。我们将首先定义输入:
 with tf.variable_scope('Input'):
 self.X = tf.placeholder(tf.int32, [None, None], name='input')
 self.Y = tf.placeholder(tf.int32, [None, None], name='target')
 self.input_shape = tf.shape(self.X)

我们必须将变量类型定义为整数,因为数据集中的单词已被转换为整数。

  1. 通过定义 LSTM 单元、词嵌入、构建 LSTM 和概率生成来定义我们的模型网络。为了定义 LSTM 单元,堆叠两个 LSTM 层,并将 LSTM 的大小设置为RNN_SIZE参数。将 RNN 的值设置为 0:
 lstm = tf.contrib.rnn.BasicLSTMCell(RNN_SIZE)
 cell = tf.contrib.rnn.MultiRNNCell([lstm] * 2) # Defining two LSTM layers for this case
 self.initial_state = cell.zero_state(self.input_shape[0], tf.float32)
 self.initial_state = tf.identity(self.initial_state, name="initial_state")

为了减少训练集的维度并提高神经网络的速度,使用以下代码生成并查找嵌入:

embedding = tf.Variable(tf.random_uniform((self.vocab_size, RNN_SIZE), -1, 1))
embed = tf.nn.embedding_lookup(embedding, self.X)

运行tf.nn.dynamic_rnn函数以找到 LSTM 的最终状态:

outputs, self.final_state = tf.nn.dynamic_rnn(cell, embed, initial_state=None, dtype=tf.float32)
self.final_state = tf.identity(self.final_state, name='final_state')

使用softmax函数将 LSTM 最终状态获得的 logits 转换为概率估计:

self.final_state = tf.identity(self.final_state, name='final_state')
self.predictions = tf.contrib.layers.fully_connected(outputs, self.vocab_size, activation_fn=None)
# Probabilities for generating words
probs = tf.nn.softmax(self.predictions, name='probs')
  1. 为 logits 序列定义加权交叉熵或序列损失,这有助于进一步微调我们的网络:
 def define_loss(self):
 # Defining the sequence loss
 with tf.variable_scope('Sequence_Loss'):
 self.loss = seq2seq.sequence_loss(self.predictions, self.Y,
 tf.ones([self.input_shape[0], self.input_shape[1]]))
  1. 使用默认参数实现 Adam 优化器,并将梯度裁剪到-11的范围内,以避免在反向传播过程中梯度消失:
 def define_optimizer(self):
 with tf.variable_scope("Optimizer"):
 optimizer = tf.train.AdamOptimizer(LEARNING_RATE)
 # Gradient Clipping
 gradients = optimizer.compute_gradients(self.loss)
 capped_gradients = [(tf.clip_by_value(grad, -1., 1.), var) for grad, var in gradients]
 self.train_op = optimizer.apply_gradients(capped_gradients)
  1. 使用generate_batch_data函数定义序列长度。这有助于生成神经网络训练所需的批次:

    • 该函数的输入将是编码为整数的文本数据、批次大小和序列长度。

    • 输出将是一个形状为[#批次,2,批次大小,序列长度]的 numpy 数组。每个批次包含两个部分,定义如下:

      • X 的形状为[批次大小,序列长度]。

      • Y 的形状为[批次大小,序列长度]:

 def generate_batch_data(int_text):
 """
 Generate batch data of x (inputs) and y (targets)
 :param int_text: Text with the words replaced by their ids
 :return: Batches as a Numpy array
 """
 num_batches = len(int_text) // (BATCH_SIZE * SEQ_LENGTH)

x = np.array(int_text[:num_batches * (BATCH_SIZE * SEQ_LENGTH)])
y = np.array(int_text[1:num_batches * (BATCH_SIZE * SEQ_LENGTH) + 1])

x_batches = np.split(x.reshape(BATCH_SIZE, -1), num_batches, 1) y_batches = np.split(y.reshape(BATCH_SIZE, -1), num_batches, 1)
 batches = np.array(list(zip(x_batches, y_batches)))
 return batches
  1. 使用以下参数训练模型:
    • 训练轮次 = 500

    • 学习率 = 0.001

    • 批次大小 = 128

    • RNN 大小 = 128

    • 序列长度 = 32:

def train(model,int_text):
# Creating the checkpoint directory
 if not os.path.exists(CHECKPOINT_PATH_DIR):
 os.makedirs(CHECKPOINT_PATH_DIR)

batches = generate_batch_data(int_text)
with tf.Session() as sess:
 if RESTORE_TRAINING:
 saver = tf.train.Saver()
 ckpt = tf.train.get_checkpoint_state(CHECKPOINT_PATH_DIR)
 saver.restore(sess, ckpt.model_checkpoint_path)
 print('Model Loaded')
 start_epoch = int(str(ckpt.model_checkpoint_path).split('-')[-1])
 else:
 start_epoch = 0
 tf.global_variables_initializer().run()
 print('All variables initialized')

for epoch in range(start_epoch, NUM_EPOCHS):
 saver = tf.train.Saver()
 state = sess.run(model.initial_state, {model.X: batches[0][0]})

for batch, (x, y) in enumerate(batches):
 feed = {
 model.X: x,
 model.Y: y,
 model.initial_state: state}
 train_loss, state, _ = sess.run([model.loss, model.final_state, model.train_op], feed)

if (epoch * len(batches) + batch) % 200 == 0:
 print('Epoch {:>3} Batch {:>4}/{} train_loss = {:.3f}'.format(
 epoch,
 batch,
 len(batches),
 train_loss))
 # Save Checkpoint for restoring if required
 saver.save(sess, CHECKPOINT_PATH_DIR + '/model.tfmodel', global_step=epoch + 1)

# Save Model
 saver.save(sess, SAVE_DIR)
 print('Model Trained and Saved')
 save_params((SEQ_LENGTH, SAVE_DIR))

由于数据集不是很大,代码是在 CPU 上执行的。我们将保存输出图,因为它将对生成书籍脚本非常有用。

生成书籍脚本

现在模型已经训练好了,我们可以玩得更开心。在本节中,我们将看到如何使用模型生成书籍脚本。使用以下参数:

  • 脚本长度 = 200 个单词

  • 起始词 = postgresql

按照以下步骤生成模型:

  1. 加载训练模型的图。

  2. 提取四个张量,如下所示:

    • 输入/input:0

    • 网络/initial_state:0

    • 网络/final_state:0

    • 网络/probs:0

使用以下代码提取四个张量:

 def extract_tensors(tf_graph):
 """
 Get input, initial state, final state, and probabilities tensor from the graph
 :param loaded_graph: TensorFlow graph loaded from file
 :return: Tuple (tensor_input,tensor_initial_state,tensor_final_state, tensor_probs)
 """
 tensor_input = tf_graph.get_tensor_by_name("Input/input:0")
 tensor_initial_state = tf_graph.get_tensor_by_name("Network/initial_state:0")
 tensor_final_state = tf_graph.get_tensor_by_name("Network/final_state:0")
 tensor_probs = tf_graph.get_tensor_by_name("Network/probs:0")
 return tensor_input, tensor_initial_state, tensor_final_state, tensor_probs
  1. 定义起始词并从图中获得初始状态,稍后会使用这个初始状态:
# Sentences generation setup
sentences = [first_word]
previous_state = sess.run(initial_state, {input_text: np.array([[1]])})
  1. 给定一个起始词和初始状态,继续通过 for 循环迭代生成脚本中的下一个单词。在 for 循环的每次迭代中,使用之前生成的序列作为输入,从模型中生成概率,并使用select_next_word函数选择概率最大的单词:
 def select_next_word(probs, int_to_vocab):
 """
 Select the next work for the generated text
 :param probs: list of probabilities of all the words in vocab which can be selected as next word
 :param int_to_vocab: Dictionary of word ids as the keys and words as the values
 :return: predicted next word
 """
 index = np.argmax(probs)
 word = int_to_vocab[index]
 return word
  1. 创建一个循环来生成序列中的下一个单词:
 for i in range(script_length):

 # Dynamic Input
 dynamic_input = [[vocab_to_int[word] for word in sentences[-seq_length:]]]
 dynamic_seq_length = len(dynamic_input[0])

# Get Prediction
 probabilities, previous_state = sess.run([probs, final_state], {input_text: dynamic_input, initial_state: previous_state})
 probabilities= np.squeeze(probabilities)

pred_word = select_next_word(probabilities[dynamic_seq_length - 1], int_to_vocab)
 sentences.append(pred_word)
  1. 使用空格分隔符将句子中的所有单词连接起来,并将标点符号替换为实际符号。然后将获得的脚本保存在文本文件中,供以后参考:
# Scraping out tokens from the words
book_script = ' '.join(sentences)
for key, token in token_dict.items():
    book_script = book_script.replace(' ' + token.lower(), key)
book_script = book_script.replace('\n ', '\n')
book_script = book_script.replace('( ', '(')
  1. 以下是执行过程中生成的文本示例:
 postgresql comparatively).
one transaction is important, you can be used

create index is seen a transaction will be provided this index.
an index scan is a lot of a index
the index is time.
to be an index.
you can see is to make expensive,
the following variable is an index

the table will index have to a transaction isolation level
the transaction isolation level will use a transaction will use the table of the following index creation.
the index is marked.
the following number is one of the following one lock is not a good source of a transaction will use the following strategies
in this is not, it will be a table
in postgresql.
the postgresql cost errors is not possible to use a transaction.
postgresql 10\. 0\. 0\. you can see that the data is not free into more than a transaction ids, the same time. the first scan is an example
the same number.
one index is not that the same time is needed in the following strategies

in the same will copy block numbers.
the same data is a table if you can not be a certain way, you can see, you will be able to create statistics.
postgresql will

有趣的是,模型学会了在句子后使用句号,在段落之间留空行,并遵循基本语法。模型通过自我学习掌握了这一切,我们无需提供任何指导或规则。尽管生成的脚本远未完美,但看到机器能够生成类似书籍的真实句子,实在令人惊讶。我们可以进一步调整模型的超参数,生成更有意义的文本。

总结

本章中,我们学习了如何使用 LSTM 生成书籍脚本。

我们从 RNN 的基础知识以及其流行变体——通常称为 LSTM 的模型开始学习。我们了解到,RNN 在预测涉及时间序列、自然语言处理任务中的下一个单词预测等顺序数据集方面非常成功。我们还了解了使用 LSTM 的优缺点。

本章帮助我们理解了如何对文本数据进行预处理,并将其准备好,以便可以输入到 LSTM 模型中。我们还了解了用于训练的模型结构。接下来,我们学习了如何通过创建数据批次来训练神经网络。

最终,我们理解了如何使用我们训练的 TensorFlow 模型生成书籍脚本。尽管生成的脚本并不完全有意义,但看到神经网络生成书籍的句子仍然令人惊叹。然后我们将生成的书籍脚本保存到文本文件中,供以后参考。

在下一章中,我们将使用深度强化学习玩吃豆人游戏。

问题

以下是问题:

  1. 你能尝试使用另一本书来看看模型生成新文本的效果如何吗?

  2. 如果你将批处理大小加倍并减小学习率,生成的文本会发生什么变化?

  3. 你能在不使用梯度裁剪的情况下训练模型,看看结果是否有所改进吗?

第十四章:使用深度强化学习玩吃豆人

强化学习是指一种范式,代理通过从环境反馈中学习,依据其所采取的动作获得观察结果和奖励。以下图展示了强化学习的基于反馈的学习循环:

尽管强化学习主要应用于学习如何玩游戏,但它也已成功应用于数字广告、股票交易、自动驾驶汽车和工业机器人等领域。

在本章中,我们将使用强化学习创建一个吃豆人游戏,并在这个过程中学习强化学习。我们将涵盖以下主题:

  • 强化学习

  • 强化学习与监督学习和无监督学习的区别

  • 强化学习的组件

  • OpenAI Gym

  • OpenAI Gym 中的吃豆人游戏

  • 深度强化学习中的 DQN:

    • Q 学习

    • 深度 Q 网络

  • 将 DQN 应用于吃豆人游戏

让我们开始吧!

强化学习

强化学习是一种机器学习方法,代理通过与环境的交互进行学习。代理采取行动,并根据这些行动,环境返回观察结果和奖励。通过这些观察结果和奖励,代理学习策略并采取进一步的行动,从而不断延续行动、观察和奖励的循环。在长期来看,代理必须学习一种策略,使其在依据策略采取行动时,能够最大化长期奖励。

强化学习与监督学习和无监督学习的区别

机器学习解决方案可以分为三种主要类型:监督学习、无监督学习和强化学习。那么强化学习与其他两种类型有何不同呢?

  1. 监督学习:在监督学习中,代理从包含特征和标签的训练数据集中学习模型。监督学习的两种最常见问题是回归和分类。回归是指根据模型预测未来的值,分类是指预测输入值的类别。

  2. 无监督学习:在无监督学习中,代理从仅包含特征的训练数据集中学习模型。无监督学习的两种最常见问题是降维和聚类。降维是指在不改变数据集自然分布的情况下,减少数据集中的特征或维度数量。聚类是指将输入数据划分为多个组,从而产生聚类或段。

  3. 强化学习:在强化学习中,代理从初始模型开始,然后根据环境反馈不断学习该模型。强化学习代理通过对一系列动作、观察和奖励应用监督或无监督学习方法来更新模型。代理仅从奖励信号中学习,而不像其他机器学习方法那样从损失函数中学习。代理在采取动作后才会收到反馈,而在其他机器学习方法中,反馈在训练时就会通过损失或错误提供。数据不是独立同分布(i.i.d.),因为它依赖于先前采取的动作,而在其他机器学习方法中,数据是独立同分布的。

强化学习的组成部分

在任何强化学习的形式化中,我们使用状态空间动作空间来讨论。动作空间是代理可以采取的有限数量的动作集合,用A表示。状态空间是环境可能处于的有限状态集合,用S表示。

代理的目标是学习一个策略,表示为 策略可以是确定性的随机的。策略基本上代表了模型,代理使用该模型来选择最优动作。因此,策略将来自环境的奖励和观察映射到动作。

当代理遵循某个策略时,会产生一个状态、动作、奖励、状态等的序列。这个序列被称为轨迹回合

强化学习形式化中的一个重要组成部分是回报。回报是对总长期奖励的估计。通常,回报可以用以下公式表示:

这里  是一个折扣因子,值在(0,1)之间, 是时间步长t时的奖励。折扣因子表示在未来的时间步长中,奖励的重要性。若  为 0,则只考虑下一动作的奖励;若为 1,则未来的奖励与下一动作的奖励具有相同的权重。

然而,由于计算回报值非常困难,因此它是通过状态值动作值函数来估计的。我们将在本章的 Q 学习部分进一步讨论动作值函数。

为了模拟我们将要玩吃豆人游戏的代理,我们将使用 OpenAI Gym。现在让我们了解一下 OpenAI Gym。

你可以在本书的代码包中的 Jupyter Notebook 文件ch-14_Reinforcement_Learning中跟随代码进行学习。

OpenAI Gym

OpenAI Gym 是一个基于 Python 的工具包,用于开发强化学习算法。在撰写本书时,它提供了超过 700 个开源贡献的环境。还可以创建自定义的 OpenAI 环境。OpenAI Gym 提供了一个统一的接口,用于处理强化学习环境的工作,同时 OpenAI 的用户可以专注于设计和实现强化学习算法。

OpenAI Gym 的原始研究论文可以在以下链接找到:arxiv.org/abs/1606.01540

让我们看一看以下步骤,学习如何安装和探索 OpenAI Gym:

  1. 使用以下命令安装 OpenAI Gym:
pip3 install gym

如果前述命令无法运行,请参考以下链接获取安装的进一步帮助:github.com/openai/gym#installation

  1. 使用以下代码打印 OpenAI Gym 中可用的环境数量:
all_env = list(gym.envs.registry.all())
print('Total Environments in Gym version {} : {}'
    .format(gym.__version__,len(all_env)))

前述代码生成了以下输出:

Total Environments in Gym version 0.10.5 : 797
  1. 打印所有环境的列表,如下代码所示:
for e in list(all_env):
    print(e)

输出的部分列表如下:

EnvSpec(Copy-v0) EnvSpec(RepeatCopy-v0) EnvSpec(ReversedAddition-v0) EnvSpec(ReversedAddition3-v0) EnvSpec(DuplicatedInput-v0) EnvSpec(Reverse-v0) EnvSpec(CartPole-v0) EnvSpec(CartPole-v1) EnvSpec(MountainCar-v0) EnvSpec(MountainCarContinuous-v0) EnvSpec(Pendulum-v0)

每个由 env 对象表示的环境都有一个标准化的接口:

  • 可以使用 env.make(<game-id-string>) 函数通过传递 id 字符串来创建 env 对象。

  • 每个 env 对象包含以下主要函数:

    • 函数 step() 接受一个动作对象作为参数,并返回四个对象:

      • observation: 环境实现的对象,表示环境的观察。

      • reward: 一个有符号浮点数,表示上一个动作的收益(或损失)。

      • done: 一个布尔值,表示场景是否结束。

      • info: 一个表示诊断信息的 Python 字典对象。

    • 函数 render() 创建环境的视觉表示。

    • 函数 reset() 将环境重置为原始状态。

  • 每个 env 对象都带有明确定义的动作和观察,分别由 action_spaceobservation_space 表示。

在 OpenAI Gym 中创建 Pacman 游戏

在本章中,我们将以 MsPacman-v0 作为示例,探索这个游戏更深入一些:

  1. 使用标准的 make 函数创建 env 对象,如下命令所示:
env=gym.make('MsPacman-v0')
  1. 让我们用以下代码打印游戏的动作空间:
print(env.action_space)

前面的代码生成了以下输出:

Discrete(9)

Discrete 9 指的是九种动作,如上、下、左、右。

  1. 现在我们可以看到观察空间,如下例所示:
print(env.observation_space)

前述代码生成了以下输出:

Box(210, 160, 3)

因此,观察空间有三个颜色通道,大小为 210 x 160。观察空间的渲染如下截图所示:

  1. 章节数是游戏次数的数量。我们现在将其设置为 1,表示我们只想玩一次游戏。由于每一轮游戏都是随机的,实际生产运行时,你会运行多轮游戏并计算奖励的平均值。让我们运行一次游戏,同时在游戏过程中随机选择一个动作,代码如下:
import time

frame_time = 1.0 / 15 # seconds
n_episodes = 1

for i_episode in range(n_episodes):
    t=0
    score=0
    then = 0
    done = False
    env.reset()
    while not done:
        now = time.time()
        if frame_time < now - then:
            action = env.action_space.sample()
            observation, reward, done, info = env.step(action)
            score += reward
            env.render()
            then = now
            t=t+1
    print('Episode {} finished at t {} with score {}'.format(i_episode,
                                                             t,score))

我们随后得到以下输出:

Episode 0 finished at t 551 with score 100.0
  1. 现在,让我们运行游戏 500 次,看看我们得到的最大分数、最小分数和平均分数。这在以下示例中得以演示:
import time
import numpy as np

frame_time = 1.0 / 15 # seconds
n_episodes = 500

scores = []
for i_episode in range(n_episodes):
    t=0
    score=0
    then = 0
    done = False
    env.reset()
    while not done:
        now = time.time()
        if frame_time < now - then:
            action = env.action_space.sample()
            observation, reward, done, info = env.step(action)
            score += reward
            env.render()
            then = now
            t=t+1
    scores.append(score)
    #print("Episode {} finished at t {} with score {}".format(i_episode,t,score))
print('Average score {}, max {}, min {}'.format(np.mean(scores),
                                          np.max(scores),
                                          np.min(scores)
                                         ))

上述代码生成了以下输出:

Average 219.46, max 1070.0, min 70.0

随机选择一个动作并应用它可能不是最佳策略。为了让智能体通过玩游戏学习并应用最佳动作,有许多算法可以用来找到解决方案。在本章中,我们将应用深度 Q 网络来从游戏中学习。我们鼓励读者探索其他算法。

DQN 用于深度强化学习

深度 Q 网络DQN)是基于 Q-learning 的。在本节中,我们将在实现 DQN 来玩 PacMan 游戏之前,先解释这两者。

  • Q-learning:在 Q-learning 中,智能体学习动作值函数,也称为 Q 函数。Q 函数表示为 q(s,a),用于估计当智能体处于状态 s 时采取动作 a 的长期价值。Q 函数将状态-动作对映射到长期价值的估计值,如下方程所示:

因此,在策略下,q 值函数可以写作如下:

q 函数可以递归地写作如下:

期望值可以展开如下:

一个最优的 q 函数是返回最大值的函数,而一个最优策略是应用最优 q 函数的策略。最优 q 函数可以写作如下:

这个方程表示贝尔曼最优方程。由于直接求解这个方程很困难,Q-learning 是一种用来逼近该函数值的方法。

因此,在 Q-learning 中,构建了一个模型,该模型能够预测给定状态和动作下的值。通常,这个模型以表格的形式存在,包含了所有可能的状态 s 和动作 a 的组合,以及该组合的期望值。然而,对于状态-动作组合数量较大的情况,这个表格就变得难以维护。DQN 有助于克服基于表格的 Q-learning 的这一缺点。

  • DQN:在 DQN 中,神经网络模型被用来从状态-动作-奖励-下一状态元组中学习,并根据提供的状态和动作预测近似的 q 值。由于状态-动作-奖励序列在时间上是相关的,深度学习面临挑战,因为深度学习中的输入样本需要是独立同分布(i.i.d.)。因此,在 DQN 算法中,经验回放被用来缓解这一问题。在经验回放中,之前的动作及其结果被随机采样,用于训练网络。

基本的深度 Q 学习算法如下:

  1. 从初始状态开始游戏

  2. 选择探索或利用

  3. 如果你选择了利用,则通过神经网络预测动作并执行预测的动作

  4. 如果你选择了探索,则随机选择一个动作

  5. 记录之前的状态、动作、奖励和下一状态到经验缓存中

  6. 使用 bellman 函数更新 q_values

  7. 使用 statesactionsq_values 训练神经网络

  8. 第 2 步开始重复

为了提高性能并实现经验回放,你可以在第 7 步中随机选择训练数据。

将 DQN 应用到游戏中

到目前为止,我们已经随机选择了一个动作并将其应用于游戏。现在,让我们应用 DQN 来选择动作,以便玩吃豆人游戏。

  1. 我们定义了 q_nn 策略函数,如下所示:
def policy_q_nn(obs, env):
    # Exploration strategy - Select a random action
    if np.random.random() < explore_rate:
        action = env.action_space.sample()
    # Exploitation strategy - Select the action with the highest q
    else:
        action = np.argmax(q_nn.predict(np.array([obs])))
    return action
  1. 接下来,我们修改 episode 函数,将 q_values 的计算和神经网络训练加入到采样的经验缓存中。代码如下所示:
def episode(env, policy, r_max=0, t_max=0):

    # create the empty list to contain game memory
    #memory = deque(maxlen=1000)

    # observe initial state
    obs = env.reset()
    state_prev = obs
    #state_prev = np.ravel(obs) # replaced with keras reshape[-1]

    # initialize the variables
    episode_reward = 0
    done = False
    t = 0

    while not done:

        action = policy(state_prev, env)
        obs, reward, done, info = env.step(action)
        state_next = obs
        #state_next = np.ravel(obs) # replaced with keras reshape[-1]

        # add the state_prev, action, reward, state_new, done to memory
        memory.append([state_prev,action,reward,state_next,done])

        # Generate and update the q_values with 
        # maximum future rewards using bellman function:
        states = np.array([x[0] for x in memory])
        states_next = np.array([np.zeros(n_shape) if x[4] else x[3] for x in memory])

        q_values = q_nn.predict(states)
        q_values_next = q_nn.predict(states_next)

        for i in range(len(memory)):
            state_prev,action,reward,state_next,done = memory[i]
            if done:
                q_values[i,action] = reward
            else:
                best_q = np.amax(q_values_next[i])
                bellman_q = reward + discount_rate * best_q
                q_values[i,action] = bellman_q

        # train the q_nn with states and q_values, same as updating the q_table
        q_nn.fit(states,q_values,epochs=1,batch_size=50,verbose=0)

        state_prev = state_next

        episode_reward += reward
        if r_max > 0 and episode_reward > r_max:
            break
        t+=1
        if t_max > 0 and t == t_max:
            break
    return episode_reward
  1. 定义一个 experiment 函数,该函数将在特定数量的回合中运行;每个回合运行直到游戏失败,即 doneTrue 时结束。我们使用 rewards_max 来表示何时退出循环,因为我们不希望实验永远运行,代码如下所示:
# experiment collect observations and rewards for each episode
def experiment(env, policy, n_episodes,r_max=0, t_max=0):

    rewards=np.empty(shape=[n_episodes])
    for i in range(n_episodes):
        val = episode(env, policy, r_max, t_max)
        #print('episode:{}, reward {}'.format(i,val))
        rewards[i]=val

    print('Policy:{}, Min reward:{}, Max reward:{}, Average reward:{}'
        .format(policy.__name__,
              np.min(rewards),
              np.max(rewards),
              np.mean(rewards)))
  1. 使用以下代码创建一个简单的 MLP 网络:
from collections import deque 
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Flatten

# build the Q-Network
model = Sequential()
model.add(Flatten(input_shape = n_shape))
model.add(Dense(512, activation='relu',name='hidden1'))
model.add(Dense(9, activation='softmax', name='output'))
model.compile(loss='categorical_crossentropy',optimizer='adam')
model.summary()
q_nn = model

上述代码生成了以下输出:

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
flatten_3 (Flatten)          (None, 100800)            0         
_________________________________________________________________
hidden1 (Dense)              (None, 8)                 806408    
_________________________________________________________________
output (Dense)               (None, 9)                 81        
=================================================================
Total params: 806,489
Trainable params: 806,489
Non-trainable params: 0
_________________________________________________________________
  1. 创建一个空列表来保存游戏记忆,并定义其他超参数,运行一个回合的实验,如下所示:
# Hyperparameters

discount_rate = 0.9
explore_rate = 0.2
n_episodes = 1

# create the empty list to contain game memory
memory = deque(maxlen=1000)

experiment(env, policy_q_nn, n_episodes)

我们得到的结果如下:

Policy:policy_q_nn, Min reward:490.0, Max reward:490.0, Average reward:490.0

在我们的案例中,这无疑是一个改进,但在你的案例中可能会有所不同。在这种情况下,我们的游戏只从有限的记忆中学习,并且仅仅通过一次回合中的游戏回放进行学习。

  1. 现在,运行 100 回合,如下所示:
# Hyperparameters

discount_rate = 0.9
explore_rate = 0.2
n_episodes = 100

# create the empty list to contain game memory
memory = deque(maxlen=1000)

experiment(env, policy_q_nn, n_episodes)

我们得到以下结果:

Policy:policy_q_nn, Min reward:70.0, Max reward:580.0, Average reward:270.5

因此,我们看到,尽管我们达到了高的最大奖励,平均结果并没有得到改善。调整网络架构、特征和超参数可能会产生更好的结果。我们鼓励你修改代码。例如,你可以用简单的单层卷积网络替代 MLP,如下所示:

from collections import deque 
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Flatten
from tensorflow.keras.layers import Conv2D, MaxPooling2D

# build the CNN Q-Network
model = Sequential()
model.add(Conv2D(16, kernel_size=(5, 5), 
                 strides=(1, 1),
                 activation='relu',
                 input_shape=n_shape))
model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))
model.add(Flatten())
model.add(Dense(512, activation='relu',name='hidden1'))
model.add(Dense(9, activation='softmax', name='output'))
model.compile(loss='categorical_crossentropy',optimizer='adam')
model.summary()
q_nn = model

上述代码显示了网络摘要,如下所示:

_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d_4 (Conv2D)            (None, 206, 156, 16)      1216      
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 103, 78, 16)       0         
_________________________________________________________________
flatten_8 (Flatten)          (None, 128544)            0         
_________________________________________________________________
hidden1 (Dense)              (None, 512)               65815040  
_________________________________________________________________
output (Dense)               (None, 9)                 4617      
=================================================================
Total params: 65,820,873
Trainable params: 65,820,873
Non-trainable params: 0

总结

在这一章中,我们学习了什么是强化学习。强化学习是一种高级技术,你会发现它常用于解决复杂问题。我们了解了 OpenAI Gym,这个框架提供了一个环境来模拟许多流行的游戏,以便实现和实践强化学习算法。我们简要介绍了深度强化学习的概念,我们鼓励你去阅读一些专门写关于强化学习的书籍(在深入阅读中提到),以便深入了解其理论和概念。

我们学习了如何在 OpenAI Gym 中玩 PacMan 游戏。我们实现了 DQN,并使用它来学习玩 PacMan 游戏。为了简化,我们只使用了一个 MLP 网络,但对于复杂的示例,你可能最终会使用复杂的 CNN、RNN 或 Sequence-to-Sequence 模型。

在下一章中,我们将学习机器学习和 TensorFlow 领域的未来机会。

深入阅读

  • Maxim Lapan 的《深度强化学习实战》,Packt 出版社

  • Richard S. Sutton 和 Andrew G. Barto 的《强化学习:导论》

  • Masashi Sugiyama 的《统计强化学习:现代机器学习方法》

  • Csaba Szepesvari 的《强化学习算法》

第十五章:接下来是什么?

恭喜你已经走到了这里。到目前为止,你已经学会了在 TensorFlow 中实现各种前沿的 AI 算法,并且在旁边做了一些很酷的项目。具体来说,我们已经构建了强化学习、贝叶斯神经网络、胶囊网络和生成对抗网络GANs)等项目。我们还学习了 TensorFlow 的几个模块,包括 TensorFlow.js、TensorFlow Lite 和 TensorFlow Probability 等等。你应该为此感到自豪,并享受片刻的休息。

在我们出去玩之前,还有一些我们应该考虑阅读的内容,确保在将这些前沿技术投入生产环境时,我们已做好充分准备。正如我们在这一章中将会意识到的那样,部署机器学习模型到生产环境中,不仅仅是实现最新的人工智能研究论文。为了理解我的意思,让我们浏览以下主题:

  • 部署模型到生产环境中的 TensorFlow 工具

  • 构建 AI 应用程序的一般规则

  • 深度学习的局限性

  • AI 在不同行业中的应用

  • 人工智能中的伦理考虑

在生产环境中实现 TensorFlow

在软件工程中,我们看到了一些最佳实践,比如通过 GitHub 进行版本控制、可重用的库、持续集成等,这些实践提高了开发人员的生产力。机器学习作为一个新兴领域,确实需要一些工具来简化模型部署,并提升数据科学家的生产力。在这方面,TensorFlow 最近发布了许多工具。

了解 TensorFlow Hub

软件仓库在软件工程领域具有实际的好处,因为它们增强了代码的可重用性。这不仅有助于提高开发人员的生产力,还帮助不同的开发人员之间共享专业知识。而且,由于开发人员现在希望共享他们的代码,他们会以更清晰和模块化的方式开发代码,从而使整个社区受益。

Google 引入了 TensorFlow Hub,旨在实现机器学习中的可重用性。它的设计目的是让你可以创建、分享和重用机器学习模型的组件。在机器学习中,可重用性比在软件工程中更为重要,因为我们不仅使用算法/架构和专业知识——我们还使用了大量用于训练模型的计算资源和数据。

TF Hub 包含了多个由 Google 专家使用最先进的算法和海量数据训练的机器学习模型。每一个经过训练的模型在 TF Hub 中被称为模块。一个模块可以在TensorFlow Hub上分享,任何人都可以将其导入到自己的代码中。下图展示了经过训练的 TensorFlow 模型如何被其他应用程序/模型使用的流程:

模块TensorFlow Hub中包含了模型的架构或TensorFlow图以及训练好的模型的权重。模块具有以下特点:

  • 可组合:可组合意味着我们可以将模块作为构建块并在其上添加其他内容。

  • 可重用:可重用意味着模块具有通用的签名,这样我们就可以交换不同的模块。这在我们迭代模型以获得最佳准确度时非常有用。

  • 可再训练:模块带有预训练的权重,但它们足够灵活,可以在新数据集上进行再训练。这意味着我们可以通过反向传播调整模型,生成新的权重集。

让我们通过一个示例来理解这一点。假设我们有一个包含不同丰田汽车(如凯美瑞、卡罗拉等型号)的数据集。如果我们每个类别的图像不多,从头开始训练整个模型并不明智。

相反,我们可以做的是,利用一个已经在 TensorFlow Hub 上使用大量图像数据集训练的通用模型,提取该模型的可重用部分,如其架构和预训练权重。在预训练的模型基础上,我们可以添加一个分类器,将数据集中存在的图像进行适当分类。这个过程有时也被称为迁移学习。下图展示了这一过程:

如果你想了解更多关于迁移学习的内容,请参考斯坦福大学的课程笔记(cs231n.github.io/transfer-learning/

你可以访问 TensorFlow Hub(www.tensorflow.org/hub/)获取最先进的、以研究为导向的图像模型,并直接将其导入到你自定义的模型中。假设我们使用的是 NasNet(tfhub.dev/google/imagenet/nasnet_large/feature_vector/1),这是一个通过架构搜索训练的图像模块。在这里,我们将在代码中使用 NasNet 模块的 URL 来导入模块,如下所示:

module = hub.Module(“https://tfhub.dev/google/imagenet/nasnet_large/feature_vect

        或/1”)

features = module(toyota_images)

logits = tf.layers.dense(features, NUM_CLASSES)

probabilities = tf.nn.softmax(logits)

我们在模块上添加了一个带有 Softmax 非线性的全连接层。我们通过反向传播训练该层的权重,以便对丰田汽车图像进行分类。

请注意,我们不需要下载该模块,也不需要实例化它。

TensorFlow 处理所有这些底层细节,这使得该模块在真正意义上是可重用的。使用该模块的另一个好处是,您可以免费获得训练 NasNet 所需的数千小时计算资源。

假设我们确实有一个大数据集。在这种情况下,我们可以按以下方式训练模块的可重用部分:

module = hub.Module(“https://tfhub.dev/google/imagenet/nasnet_large/feature_vector/1”, trainable=True, tags {“train”})features = module(toyota_images)

logits = tf.layers.dense(features, NUM_CLASSES)

probabilities = tf.nn.softmax(logits)

TensorFlow Hub 提供了用于图像分类、词嵌入、句子嵌入和其他应用的预训练模型。让我们考虑一下来自本书第三章的电影情感分析项目,使用 Tensorflow.js 在浏览器中进行情感分析。我们本可以使用 TensorFlow Hub 提供的每个数据集项的预训练嵌入。这些跨领域的预训练模块的可用性将帮助许多开发人员在不必担心模型背后的数学原理的情况下构建新应用。

您可以在 TensorFlow Hub 的官方网站上找到更多详细信息(www.tensorflow.org/hub/)。

TensorFlow Serving

TensorFlow Serving 是一个高度灵活的服务系统,用于在生产环境中部署机器学习模型。在详细介绍之前,我们先通过查看其架构来理解什么是服务:

我们有一些数据,并使用这些数据训练一个机器学习模型。一旦模型训练完成,它需要部署到网页或移动应用程序上,以服务最终用户。实现这一目标的一种方式是通过远程过程调用RPC)服务器(www.ibm.com/support/knowledgecenter/en/ssw_aix_72/com.ibm.aix.progcomc/ch8_rpc.htm)。TensorFlow Serving 可以作为RPC 服务器和一组库使用,可以在应用程序或嵌入式设备中使用。

TensorFlow Serving 有三个支柱:

  • C++ 库: 低级 C++库主要包含了 TensorFlow 服务所需的函数和方法。这些库是 Google 用来生成应用程序使用的二进制文件的库,它们也是开源的。

  • 二进制文件: 如果我们希望为我们的服务架构使用标准设置,可以使用预定义的二进制文件,这些文件包含了来自 Google 的所有最佳实践。Google 还提供了 Docker 容器(www.docker.com/)以便在 Kubernetes 上扩展这些二进制文件(kubernetes.io/)。

  • 托管服务: TensorFlow Serving 还在 Google Cloud ML 上提供托管服务,这使得使用和部署变得非常容易。

以下是 TensorFlow Serving 的一些优点:

  • 在线和低延迟: 用户不希望在应用程序中等待预测结果。使用 TF Serving,预测不仅快速,而且始终如一地保持快速。

  • 单一进程中的多个模型: TF Serving 允许在同一进程中加载多个模型。假设我们有一个模型,它为客户提供了很好的预测。但如果我们想进行实验,那么我们可能希望加载另一个模型,并与生产模型一起使用。

  • 自动加载和训练同一模型的版本: TF Serving 支持在不中断服务的情况下自动加载新训练的模型,并从生产中的旧版本切换到新版本。

  • 可扩展性: TF Serving 可以与 Cloud ML、Docker 和 Kubernetes 自动扩展。

如需了解如何使用 TF Serving 部署您的模型,请参考官方文档这里www.tensorflow.org/serving/)。

TensorFlow Extended

TensorFlow ExtendedTFX)是 Google 构建的通用机器学习平台。其部分组件是开源的,并且最近在 KDD 会议上有一篇论文(www.kdd.org/kdd2017/papers/view/tfx-a-tensorflow-based-production-scale-machine-learning-platform)展示了 TFX 的功能和愿景。

在本书中,我们主要理解了构建 TensorFlow 模型的语义。然而,当我们查看实际的生产中的机器学习应用时,还有许多其他组件。以下图示展示了这些组件:

如我们所见,机器学习代码是整个系统中非常小的组成部分。其他模块需要最大时间来构建,并且占用了最多的代码行数。TFX 提供了构建机器学习管道其他组件的库和工具。

让我们看一个机器学习过程的示例,以了解 TensorFlow Extended 的不同开源组件:

  • 数据分析: 探索性数据分析是构建任何机器学习模型的必要条件。TFX 有一个名为 Facets 的工具(github.com/PAIR-code/facets),它让我们可视化每个变量的分布,识别缺失数据或异常值,或者通知他人数据可能需要的转换。

  • 转换: TensorFlow 转换(www.tensorflow.org/tfx/transform/get_started)提供开箱即用的功能,能够对基础数据进行完整的转换,使其适合用于训练模型。它与 TF 图本身紧密相关,确保在训练和服务过程中应用相同的转换。

  • 训练 TF 估算器:在数据转换之后,我们可以使用 TF 估算器 (www.tensorflow.org/api_docs/python/tf/estimator/Estimator),它提供了一个高层次的 API,可以快速定义、训练和导出模型。TF 估算器还允许你以不同的格式导出模型进行推理和服务。

  • 分析模型:一旦模型构建完成,我们可以直接将其推送到生产环境,但这会是一个非常糟糕的主意。相反,我们应该分析模型的预测结果,并确保模型预测的是我们希望它预测的内容。TF 模型分析 (www.tensorflow.org/tfx/model_analysis/get_started) 使我们能够在大规模数据集上评估模型,并提供一个用户界面,可以根据不同的属性值切分预测结果。

  • 服务模型:在分析我们的模型并对其预测结果感到满意后,我们希望将模型部署到生产环境中。实现这一目标的一种方式是使用 TensorFlow Serving,这在上一节中已有描述。

TensorFlow Extended 在 Google 内部广泛用于构建产品。它显然比开源的版本有更多功能。对于那些在初创公司或没有自己内部机器学习平台的公司工作的人来说,强烈推荐使用 TFX 来构建端到端的机器学习应用。

构建 AI 应用的建议

现在我们了解了一些来自 TensorFlow 的工具,这些工具能帮助我们在大规模下开发和部署模型,让我们尝试理解构建 AI 应用时的一些通用经验法则。

  • 工程优先于机器学习:几乎所有问题的解决方案都始于工程。在构建任何机器学习模型之前,确保数据管道正确非常重要。

  • 保持简单:通常,数据科学家有一种自然倾向,倾向于为问题构建最复杂的模型。然而,从简单且可解释的模型开始是非常好的——比如说,使用逻辑回归模型进行分类。这有助于更好地发现和调试数据或工程管道中的问题。只有当你对基础模型的结果不满意时,才应该使用像深度学习这样的高级技术。

  • 分布式处理:在大数据时代,你几乎总会遇到无法将数据装入内存的问题。了解像 Spark 这样的分布式框架在处理和构建可扩展的机器学习应用时会大有帮助。

  • 自动化模型再训练:一旦模型部署,它的性能可能会随着时间的推移而下降。保持检查模型的准确性非常重要,以便可以使用新数据启动自动训练。这将有助于维持产品的预测准确性。

  • 训练和测试管道:对于独立的训练和测试管道,总是有可能导致训练和测试特征之间的偏差。尽量确保训练和测试管道之间有尽可能多的重叠。这可以帮助更轻松地调试模型预测。

  • 通过 A/B 测试推出新模型:A/B 测试是一种比较两个版本模型/网页等的方法。它是一个统计实验,其中随机向用户展示两个不同版本。你可以在普渡大学的讲义中阅读更多相关内容(www.cs.purdue.edu/homes/ribeirob/courses/Fall2016/lectures/hyp_tests.pdf)。

如果你构建的模型优于已经在生产环境中使用的模型,你会在测试数据集上看到准确度的提升。然而,由于各种问题(如相关性与因果关系、用户行为变化等),在生产环境中你可能不会看到与现有模型相同的提升。因此,在将新模型推出给所有用户之前,在生产环境中进行 A/B 测试是非常重要的。

  • 单一模型优于集成模型:集成模型(多个单一模型的组合)可能会比单一模型提供更好的准确性。然而,如果增益不显著,始终建议使用单一模型。这是因为集成模型在生产系统中难以维护、调试和扩展。

深度学习的局限性

在这个项目中,几乎所有的项目都涉及某种深度学习。深度学习在推动过去几年大部分进展中起到了关键作用。然而,深度学习存在一些显而易见的局限性,我们在将其应用于现实世界场景之前应当理解这些限制。以下是其中一些:

  • 数据饥渴:通常情况下,我们无法为每个需要使用机器学习解决的问题提供大规模的数据集。相反,深度学习算法只有在我们拥有庞大的数据集时才能发挥作用。

  • 计算密集型:深度学习训练通常需要 GPU 支持和大量内存。然而,这使得在像手机、平板电脑等边缘设备上训练深度神经网络变得不可能。

  • 没有预测不确定性:默认情况下,深度学习算法难以表现不确定性。深度神经网络可能会非常自信地错误地将一张猫的图片分类为狗的图片。

对于预测结果,没有置信区间或不确定性的概念。对于像自动驾驶汽车这样的应用,在做出任何决策之前考虑不确定性是非常重要的。在本书中,我们介绍了贝叶斯神经网络等概念,试图在深度神经网络中融入不确定性。

  • 不可解释的黑箱:深度学习模型很难解释和信任。例如,银行的贷款部门基于个人的过去购买记录或信用历史,通过深度神经网络来决定是否向个人发放贷款。

如果模型拒绝贷款,银行必须向个人解释为何贷款被拒。然而,使用深度神经网络时,几乎不可能提供明确的理由解释为何贷款被拒。不可解释性是这些模型未能广泛应用于各个行业的主要原因。

行业内的 AI 应用

AI 是每个公司都在努力转型的全新范式。根据麦肯锡报告(www.mckinsey.com/featured-insights/artificial-intelligence/notes-from-the-ai-frontier-modeling-the-impact-of-ai-on-the-world-economy),到 2030 年,预计 70% 的公司将至少采用一种 AI 技术。让我们看看不同行业中的 AI 应用:

  • 零售业

    • 供应链优化

    • 通过微定位定制购物体验

    • 产品定价及节假日折扣计算

    • 在零售店中定制产品摆放以增加销售

  • 社交网络(Facebook, LinkedIn, Twitter)

    • 朋友/关注者推荐

    • 基于过去历史的主页推荐定制,以提高参与度

    • 假新闻/欺诈检测

  • 医疗健康

    • 新药发现

    • 自动化医学影像处理

    • 通过 Apple Watch 或其他设备存储的数据推荐锻炼/食物

  • 金融业

    • 股票市场预测

    • 信用卡欺诈检测

    • 贷款资格

    • 客户支持聊天机器人

  • 制造业

    • 预测性维护

    • 需求预测

    • 库存管理

  • 物流

    • ETA 优化

    • 高峰定价

    • 共享出行/拼车

    • 定价

    • 自动驾驶汽车

AI 的伦理考量

我们正见证人工智能及其应用的非凡崛起。然而,AI 应用的日益复杂化引发了关于偏见、公平、安全、透明度和问责制等一系列问题。这主要是因为 AI 模型没有良知,无法独立区分好坏。它们的效果取决于它们训练所用的数据。因此,如果数据在某些方面存在偏见,那么预测结果也会偏见。还有关于因自动化导致的失业增加、AI 被用于恐怖主义以及 AI 模型的种族歧视预测等问题。

好消息是,许多大学正在投入时间和资源,寻求如何使 AI 更加公平并去除偏见的解决方案。同时,监管机构也在努力制定新的规则,以确保 AI 应用对人类来说是安全和可靠的。

作为一名 AI 从业者,我们必须在将 AI 应用于自己的产品之前理解这些问题。我敦促你关注你产品中的伦理问题,并相应地加以纠正。

总结

在这一章中,我们看到了 TensorFlow 的各种扩展,旨在提高数据科学家的生产力,并使前沿模型在大规模生产环境中更易部署。

我们了解了 TensorFlow Hub,它类似于 GitHub 上来自各个领域(如计算机视觉、自然语言处理等)的训练深度学习模型的仓库。之后,我们理解了 TensorFlow Serving 如何提供工具和库来大规模部署深度学习模型。最后,我们学习了TensorFlow ExtendedTFX)的开源组件,TFX 是谷歌的机器学习平台。TFX 帮助整个模型构建管道,从数据分析到模型部署。

接下来,我们了解了构建可扩展 AI 产品时的一些最佳实践。构建稳健的工程管道,在深度学习之前尝试简单的模型,并始终通过 A/B 测试发布新模型是其中的一些做法。

随后,我们通过了解深度神经网络的局限性,打破了关于深度学习的炒作。具体来说,我们了解到,它们需要大量的数据和计算能力来构建好的、准确的模型。此外,它们不可解释的特点使得它们在许多 AI 应用中无法使用。我们还了解了 AI 在各行各业中的应用,并学习了 AI 伦理的重要性。

最后,如果你已经看到这里并完成了项目,我感谢你,并祝贺你取得了非凡的成就。你已经掌握了使用强化学习、计算机视觉和自然语言处理等先进技术构建实用 AI 应用所需的技能。我希望你现在能将这些知识用于善事,让这个世界变得更美好。

我想以我最喜欢的名言结束本书:

未来不是我们要去的地方,而是我们在创造的地方。

  • 约翰·H·斯卡尔
posted @ 2025-07-08 21:22  绝不原创的飞龙  阅读(16)  评论(0)    收藏  举报