JavaScript-深度学习(五)

93 阅读1小时+

JavaScript 深度学习(五)

原文:zh.annas-archive.org/md5/ea99677736c22d68b5818a18b5a9213a

译者:飞龙

协议:CC BY-NC-SA 4.0

第十四章:总结、结论和展望

本章包括

  • 回顾人工智能和深度学习的高层次概念和想法

  • 我们在本书中访问的不同类型的深度学习算法的快速概述,它们何时有用,以及如何在 TensorFlow.js 中实现它们

  • 来自 TensorFlow.js 生态系统的预训练模型

  • 深度学习当前存在的局限性;以及我们将在未来几年看到的深度学习趋势的教育预测

  • 如何进一步提升你的深度学习知识并跟上这个快速发展的领域的指导

这是本书的最后一章。之前的章节是对当前深度学习领域的总体概况,通过 TensorFlow.js 和你自己的努力实现。通过这段旅程,你可能已经学到了很多新的概念和技能。现在是时候再次退后一步,重新审视全局,并对你学到的一些最重要的概念进行复习。这最后一章将总结和审查核心概念,同时将你的视野扩展到迄今为止学到的相对基本的概念之外。我们希望确保你意识到这一点,并且准备好自己继续旅程的下一步。

我们将从鸟瞰视角开始,总结你应该从这本书中学到的东西。这应该让你记起你学到的一些概念。接下来,我们将概述深度学习的一些关键局限性。要正确使用工具,你不仅应该知道它 做什么,还应该知道它 不能 做什么。本章以资源列表和进一步了解深度学习和 JavaScript 生态系统中人工智能知识和技能的策略结束,并保持与新发展的步伐同步。

13.1. 复习重点概念

这一部分简要总结了这本书的关键要点。我们将从人工智能领域的整体格局开始,以为什么将深度学习和 JavaScript 结合起来会带来独特而令人兴奋的机遇而结束。

13.1.1. 人工智能的各种方法

首先,深度学习与人工智能甚至与机器学习并不是同义词。 人工智能 是一个历史悠久的广泛领域。它通常可以定义为“所有试图自动化认知过程”的尝试——换句话说,思维的自动化。这可以从非常基本的任务,如 Excel 电子表格,到非常高级的努力,如一个可以行走和说话的类人机器人。

机器学习 是人工智能的许多子领域之一。它旨在通过暴露给训练数据自动开发程序(称为 模型)。这个将数据转化为程序(模型)的过程被称为 学习。尽管机器学习已经存在了很长一段时间(至少有几十年了),但它直到 1990 年代才开始在实际应用中蓬勃发展。

深度学习 是机器学习的众多形式之一。在深度学习中,模型由许多步骤的表示转换组成,依次应用(因此有形容词“深度”)。这些操作被结构化为称为 的模块。深度学习模型通常是许多层的堆栈或更一般地说是许多层的图。这些层由 权重 参数化,数字值有助于将层的输入转换为其输出,并在训练过程中更新。模型在训练过程中学到的“知识”体现在其权重中。训练过程主要是为这些权重找到一个良好的值。

尽管深度学习只是机器学习的众多方法之一,但与其他方法相比,它已被证明是一个突破性的成功。让我们快速回顾一下深度学习成功背后的原因。

13.1.2. 使深度学习在机器学习子领域中脱颖而出的原因

仅仅在几年的时间里,深度学习在多个历史上被认为对计算机极其困难的任务上取得了巨大突破,特别是在机器感知领域——即从图像、音频、视频和类似感知数据中提取有用信息的能力,具有足够高的准确性。如果有足够的训练数据(特别是标记的训练数据),现在可以从感知数据中提取几乎任何人类可以提取的东西,有时甚至准确度超过人类。因此,有时说深度学习在很大程度上“解决了感知”问题,尽管这只适用于对感知的一个相当狭义的定义(参见 第 13.2.5 节 以了解深度学习的局限性)。

由于其空前的技术成功,深度学习独自引发了第三次,迄今为止最大的 AI 夏季,也被称为 深度学习革命,这是人工智能领域的一个充满兴趣、投资和炒作的时期。这一时期是否会在不久的将来结束,以及之后会发生什么,是人们猜测和讨论的话题。但有一点是确定的:与以往的 AI 夏季形成鲜明对比,深度学习为许多技术公司提供了巨大价值,实现了人类级别的图像分类、目标检测、语音识别、智能助手、自然语言处理、机器翻译、推荐系统、自动驾驶汽车等。炒作可能会减退(理所当然),但深度学习的持续技术影响和经济价值将会保持。从这个意义上说,深度学习可能类似于互联网:可能在几年内被过度炒作,导致不合理的期望和过度投资,但从长远来看,它将成为一个影响技术许多领域并改变我们生活的重大革命。

我们对深度学习特别乐观,因为即使在未来十年内我们在其中不再取得进一步的学术进展,将现有的深度学习技术应用于每一个适用的实际问题仍将改变许多行业的游戏规则(在线广告、金融、工业自动化和残疾人辅助技术,只是列举了一部分)。深度学习无疑是一场革命,目前的进展速度之快令人难以置信,这要归功于资源和人员的指数级投资。从我们的角度来看,未来看起来很光明,尽管短期内的预期可能有些过于乐观;充分发挥深度学习的潜力将需要超过十年的时间。

13.1.3. 怎样从高层次上思考深度学习

深度学习最令人惊讶的一个方面是它的简单性,考虑到它的工作效果以及之前的更复杂的机器学习技术的效果并不如人意。十年前,没有人预料到我们能够仅通过使用梯度下降训练的参数模型在机器感知问题上取得如此惊人的结果。现在事实证明,你只需要足够大的通过梯度下降训练的参数模型以及足够多的带标签示例。正如理查德·费曼曾经关于他对宇宙的理解所说:“这并不复杂,只是有很多。”^([1])

¹

理查德·费曼,采访,“另一种视角下的世界”,约克郡电视台,1972 年。

在深度学习中,一切都被表示为一系列数字——换句话说,是一个向量。一个向量可以被看作是几何空间中的一个。模型输入(表格数据、图像、文本等)首先被向量化,或者转换为输入向量空间中的一组点。同样地,目标(标签)也被向量化,并转换为其相应的目标向量空间中的一组点。然后,深度神经网络中的每一层对通过它的数据执行一个简单的几何变换。在一起,神经网络中的层链形成了一个由一系列简单的几何变换组成的复杂几何变换。这个复杂的变换试图将输入向量空间中的点映射到目标向量空间中的点。这个变换由层的权重参数化,这些权重根据当前变换的好坏进行迭代更新。这个几何变换的一个关键特征是它是可微的:这就是梯度下降变得可能的原因。

13.1.4. 深度学习的关键技术

正在进行的深度学习革命并非一夜之间开始。相反,和其他任何革命一样,它是一系列因素的积累——一开始缓慢,然后一旦到达关键点就突然加速。就深度学习而言,我们可以指出以下关键因素:

  • 渐进式的算法创新,首先涉及两个十年,然后随着深度学习在 2012 年之后投入更多研究力量而加速发展。

    ²

    起始于 Rumelhart、Hinton 和 Williams 的反向传播算法,LeCun 和 Bengio 的卷积层,以及 Graves 和 Schmidthuber 的循环网络。

    ³

    例如,改进的权重初始化方法,新的激活函数,dropout,批量归一化,残差连接。

  • 大量的标记数据可用,覆盖许多数据模式,包括知觉(图像、音频和视频)、数字和文本,这使大型模型可以在足够数量的数据上进行训练。这是消费互联网兴起的副产品,由流行的移动设备推动,以及存储介质中的摩尔定律。

  • 快速、高度并行化的计算硬件以低成本提供,尤其是 NVIDIA 生产的 GPU——首先是为并行计算重新用途的游戏 GPU,然后是从头设计用于深度学习的芯片。

  • 复杂的开源软件堆栈使许多人类开发者和学习者可以使用这种计算能力,同时隐藏了庞大的复杂性:CUDA 语言、WebGL 着色器语言以及框架,如 TensorFlow.js、TensorFlow 和 Keras,其执行自动差分并提供易于使用的高级搭建块,如层、损失函数和优化器。深度学习正在从专家领域(研究人员、AI 研究生和具有学术背景的工程师)转变为每位程序员的工具。TensorFlow.js 在这方面是一个典范性的框架。它将两个丰富活跃的生态系统结合在一起:JavaScript 跨平台生态系统和快速发展的深度学习生态系统。

深度学习革命的广泛和深远影响之一是它与其他不同于它的技术栈(如 C++ 和 Python 生态系统和数字计算领域)的融合。它与 JavaScript 生态系统的跨界融合是其中的一个典型例子。在接下来的部分,我们将回顾为什么将深度学习引入 JavaScript 世界将开启令人兴奋的新机遇和可能性的关键原因。

13.1.5 用 JavaScript 实现深度学习的应用与机遇

训练深度学习模型的主要目的是使其可供用户使用。对于许多类型的输入模态,例如来自网络摄像头的图像、来自麦克风的声音以及用户输入的文本和手势输入,数据是由客户端生成并直接可用的。JavaScript 或许是最成熟和普及的客户端编程语言和生态系统。用 JavaScript 编写的相同代码可以部署为网页和 UI,在各种设备和平台上运行。Web 浏览器的 WebGL API 允许在各种 GPU 上进行跨平台并行计算,TensorFlow.js 利用了这一点。这些因素使 JavaScript 成为部署深度学习模型的一种吸引人的选择。TensorFlow.js 提供了一个转换工具,允许您将使用流行的 Python 框架(如 TensorFlow 和 Keras)训练的模型转换为适合 Web 的格式,并将其部署到网页上进行推理和迁移学习。

除了部署的便利性之外,使用 JavaScript 提供服务和微调深度学习模型还有许多其他优势:

  • 与服务器端推理相比,客户端推理放弃了双向数据传输的延迟,有利于可用性,并带来更流畅的用户体验。

  • 通过使用设备 GPU 加速在边缘执行计算,客户端深度学习消除了管理服务器端 GPU 资源的需要,显着降低了技术堆栈的复杂性和维护成本。

  • 通过保持数据和推理结果在客户端,用户的数据隐私得到了保护。这对于医疗保健和时尚等领域至关重要。

  • 浏览器和其他基于 JavaScript 的 UI 环境的视觉和交互性为神经网络的可视化、辅助理解和教学提供了独特的机会。

  • TensorFlow.js 不仅支持推理,还支持训练。这为客户端迁移学习和微调打开了大门,从而实现了机器学习模型的更好个性化。

  • 在 Web 浏览器中,JavaScript 提供了一个独立于平台的 API,用于访问设备传感器,如网络摄像头和麦克风,这加速了使用这些传感器输入的跨平台应用程序的开发。

除了其客户端的卓越性能,JavaScript 还将其能力扩展到服务器端。特别是,Node.js 是 JavaScript 中高度流行的用于服务器端应用的框架。使用 Node.js 版本的 TensorFlow.js(tfjs-node),您可以在网页浏览器之外的环境中训练和提供深度学习模型,因此不受资源限制。这利用了 Node.js 的庞大生态系统,并简化了社区成员的技术堆栈。所有这些都可以通过使用与您为客户端编写的基本相同的 TensorFlow.js 代码来实现,这使您更接近“一次编写,到处运行”的愿景,正如本书中的几个示例所示。

13.2. TensorFlow.js 中深度学习工作流程和算法的快速概述

通过历史概述,让我们现在来看看 TensorFlow.js 的技术方面。在本节中,我们将回顾当你面对一个机器学习问题时应该遵循的一般工作流程,并强调一些最重要的考虑因素和常见陷阱。然后,我们将过一下我们在书中涵盖的各种神经网络构建块(层)。此外,我们将调查 TensorFlow.js 生态系统中的预训练模型,这些模型可以加速您的开发周期。为了结束本节,我们将介绍您可以通过使用这些构建块解决的一系列机器学习问题,激励您想象使用 TensorFlow.js 编写的深度神经网络如何帮助您解决自己的机器学习问题。

13.2.1. 监督深度学习的通用工作流程

深度学习是一个强大的工具。但或许有些令人惊讶的是,机器学习工作流程中最困难和耗时的部分通常是设计和训练这些模型之前的一切(以及对于部署到生产环境的模型来说,还有之后的一切)。这些困难的步骤包括充分了解问题域,以便能够确定需要什么样的数据,以及可以以合理的准确性和泛化能力进行什么样的预测,机器学习模型如何适用于解决实际问题的整体解决方案中,以及如何度量模型在完成其工作时成功的程度。尽管这些是任何成功应用机器学习的先决条件,但它们不是像 TensorFlow.js 这样的软件库可以自动化的内容。作为提醒,以下是典型监督学习工作流程的快速摘要:

  1. 确定机器学习是否是正确的方法。首先,考虑一下机器学习是否是解决您的问题的正确方法,只有在答案是肯定的情况下才继续以下步骤。在某些情况下,非机器学习方法同样有效,甚至可能更好,成本更低。

  2. 定义机器学习问题。确定可用的数据类型以及您试图使用数据预测的内容。

  3. 检查您的数据是否足够。确定您已经拥有的数据量是否足以进行模型训练。您可能需要收集更多数据或雇用人员手动标记一个未标记的数据集。

  4. 确定一种可靠地衡量训练模型成功的方法。对于简单任务,这可能只是预测准确率,但在许多情况下,它将需要更复杂、特定于领域的度量标准。

  5. 准备评估过程。设计用于评估模型的验证过程。特别是,您应该将数据分成三个同质但不重叠的集合:一个训练集、一个验证集和一个测试集。验证集和测试集的标签不应泄漏到训练数据中。例如,对于时间预测,验证和测试数据应来自训练数据之后的时间间隔。您的数据预处理代码应该经过测试,以防止错误(第 12.1 节)。

  6. 将数据向量化。将您的数据转换为张量,或者n维数组——TensorFlow.js 和 TensorFlow 等框架中机器学习模型的通用语言。通常需要对张量化的数据进行预处理,以使其更适合您的模型(例如,通过归一化)。

  7. 超越常识基线。开发一个能够超越非机器学习基线的模型(例如,在回归问题中预测人口平均值或在时间序列预测问题中预测最后一个数据点),从而证明机器学习确实可以为您的解决方案增加价值。这并不总是可能的(见步骤 1)。

  8. 开发具有足够容量的模型。通过调整超参数和添加正则化逐渐完善模型架构。仅基于验证集的预测准确率进行更改,而不是基于训练集或测试集。请记住,您应该使您的模型过度拟合问题(在训练集上达到比验证集更好的预测准确率),从而确定一个比您需要的容量更大的模型容量。只有在那之后,您才应该开始使用正则化和其他方法来减少过拟合。

  9. 调整超参数。在调整超参数时要注意验证集的过拟合。因为超参数是基于验证集上的性能确定的,所以它们的值会过度专门化于验证集,因此可能不会很好地推广到其他数据。测试集的目的是在调整超参数后获得模型准确性的无偏估计。因此,在调整超参数时不应使用测试集。

  10. 验证和评估训练好的模型。正如我们在第 12.1 节中讨论的,用最新的评估数据集测试您的模型,并决定预测精度是否达到为实际用户服务的预定标准。此外,对模型在不同数据切片(子集)上的质量进行更深入的分析,旨在检测任何不公平行为(例如在不同数据切片上的精度差异)或不希望的偏差。^([4]) 只有当模型通过这些评估标准时,才进行最终步骤。

    机器学习中的公平性是一个新兴的研究领域;更多讨论请参见以下链接mng.bz/eD4Q

  11. 优化和部署模型。对模型进行优化,以缩小其尺寸并提高其推理速度。然后将模型部署到服务环境中,如网页、移动应用程序或 HTTP 服务端点(第 12.3 节)。

这个教程是关于监督学习的,它在许多实际问题中都会遇到。本书涵盖的其他类型的机器学习工作流包括(监督)迁移学习、RL(强化学习)和生成式深度学习。监督迁移学习(第五章)与非迁移监督学习的工作流相同,唯一的区别是模型设计和训练步骤是基于预训练模型构建的,并且可能需要比从头开始训练模型更少的训练数据。生成式深度学习的目标与监督学习有所不同——即尽可能创建看起来像真实的假例。实际上,有一些技术将生成模型的训练转化为监督学习,就像我们在第九章中看到的 VAE 和 GAN 示例一样。另一方面,RL 包含一个根本不同的问题形式化,并因此具有截然不同的工作流程——其中主要参与者是环境、代理、行动、奖励结构以及用于解决问题的算法或模型类型。第十一章提供了 RL 中基本概念和算法的快速概述。

13.2.2. 回顾 TensorFlow.js 中的模型和层类型:快速参考

本书涵盖的所有众多神经网络可以分为三类系列:密集连接网络(也称为 MLPs 或多层感知器)、卷积网络和循环网络。这三种基本系列的网络是每个深度学习实践者都应该熟悉的。每种类型的网络适用于特定类型的输入:网络架构(MLP、卷积或循环)对输入数据的结构进行假设—通过反向传播和超参数调整来搜索好模型的假设空间。给定问题是否适用于给定架构完全取决于数据中的结构与网络架构的假设匹配得有多好。

这些不同类型的网络可以像积木一样轻松地组合成更复杂和多模态的网络。从某种意义上说,深度学习层是可微分信息处理的积木。我们快速概述输入数据的模态与适当网络架构之间的映射:

  • 向量数据(没有时间序列或串行顺序)—MLPs(密集 layers

  • 图像数据(黑白、灰度或彩色)—2D 卷积网络

  • 音频数据作为频谱图—2D 卷积网络或 RNNs

  • 文本数据—1D 卷积网络或 RNNs

  • 时间序列数据—1D 卷积网络或 RNNs

  • 体积数据(例如 3D 医学图像)—3D 卷积网络

  • 视频数据(图像序列)—要么是 3D 卷积网络(如果你需要捕捉运动效果),要么是一个逐帧 2D 卷积网络用于特征提取,随后是一个 RNN 或 1D 卷积网络来处理生成的特征序列的组合

现在让我们深入了解每个主要架构系列,它们擅长的任务以及如何通过 TensorFlow.js 使用它们。

密集连接网络和多层感知器

密集连接网络多层感知机这两个术语在很大程度上可以互换使用,但要注意密集连接网络可以只包含一个层,而多层感知机必须至少包含一个隐藏层和一个输出层。为了简洁起见,我们将使用MLP这个术语来指代主要由密集层构建的所有模型。此类网络专门用于无序向量数据(例如,在钓鱼网站检测问题和房价预测问题中的数值特征)。每个密集层试图对输入特征的所有可能的对和该层的输出激活之间的关系进行建模。这通过密集层的核与输入向量之间的矩阵乘法(然后加上偏差向量和激活函数)来实现。每个输出激活受到每个输入特征的影响是这些层和建立在它们上面的网络被称为密集连接(或被一些作者称为完全连接)的原因。这与其他类型的架构(卷积网络和循环神经网络)形成对比,在这些架构中,一个输出元素仅依赖于输入数据中的一部分元素。

MLP 最常用于分类数据(例如,输入特征是属性列表,比如在钓鱼网站检测问题中)。它们也经常被用作大多数分类和回归神经网络的最终输出阶段,这些网络可以包含卷积或循环层作为特征提取器,将特征输入馈送到这样的 MLP 中。例如,我们在第四章和第五章中介绍的二维卷积网络都以一个或两个密集层结尾,我们在第九章中介绍的循环网络也是如此。

让我们简要回顾一下在监督学习中如何选择 MLP 的输出层激活函数以适应不同类型的任务。要进行二元分类,你的 MLP 的最后一个密集层应该正好有一个单元,并使用 sigmoid 激活函数。在训练这样一个二元分类器 MLP 时,应该使用binaryCrossentropy作为损失函数。你的训练数据中的例子应该有二元标签(0 或 1 的标签)。具体来说,TensorFlow.js 的代码如下:

import * as tf from '@tensorflow/tfjs';

const model = tf.sequential();
model.add(tf.layers.dense({units: 32, activation: 'relu', inputShape:
     [numInputFeatures]}));
model.add(tf.layers.dense({units: 32, activation: 'relu'}));
model.add(tf.layers.dense({units: 1: activation: 'sigmoid'}));
model.compile({loss: 'binaryCrossentropy', optimizer: 'adam'});

要进行单标签多类别分类(其中每个例子在多个候选类别中只有一个类别),请在你的层堆叠的末尾加上一个包含 softmax 激活函数的密集层,单位数量等于类别的数量。如果你的目标是独热编码,请使用categoricalCrossentropy作为损失函数;如果它们是整数索引,请使用sparseCategoricalCrossentropy。例如:

const model = tf.sequential();
model.add(tf.layers.dense({units: 32, activation: 'relu', inputShape:
     [numInputFeatures]});
model.add(tf.layers.dense({units: 32, activation: 'relu'});
model.add(tf.layers.dense({units: numClasses: activation: 'softmax'});
model.compile({loss: 'categoricalCrossentropy', optimizer: 'adam'});

为了执行多标签多类别分类(每个示例可能具有多个正确类别),将你的层堆栈结束为一个包含 sigmoid 激活和单位数量等于所有候选类别数量的密集层。使用binaryCrossentropy作为损失函数。你的目标应该是 k-hot 编码:

const model = tf.sequential();
model.add(tf.layers.dense({units: 32, activation: 'relu', inputShape:
     [numInputFeatures]}));
model.add(tf.layers.dense({units: 32, activation: 'relu'}));
model.add(tf.layers.dense({units: numClasses: activation: 'sigmoid'}));
model.compile({loss: 'binaryCrossentropy', optimizer: 'adam'});

为了对连续值向量执行回归,将你的层堆栈结束为一个具有单位数量等于你尝试预测的值数量(通常只有一个数字,比如房价或温度值)的密集层,并使用默认的线性激活函数。多个损失函数可用于回归。最常用的是meanSquaredErrormeanAbsoluteError

const model = tf.sequential();
model.add(tf.layers.dense({units: 32, activation: 'relu', inputShape:
     [numInputFeatures]}));
model.add(tf.layers.dense({units: 32, activation: 'relu'}));
model.add(tf.layers.dense({units: numClasses}));
model.compile({loss: 'meanSquaredError', optimizer: 'adam'});
卷积网络

卷积层通过将相同的几何变换应用于输入张量中的不同空间位置(补丁)来查看局部空间模式。这导致了是平移不变的表示,使卷积层高度数据有效和模块化。这个想法适用于任何维度的空间:1D(序列),2D(图像或类似于非图像数量的表示,如声音频谱图),3D(体积)等等。你可以使用tf.layers.conv1d层来处理序列,使用 conv2d 层来处理图像,使用 conv3d 层来处理体积。

卷积网络由一系列卷积和池化层组成。池化层允许你对数据进行空间降采样,这对于保持特征图的合理大小是必需的,因为特征数量增加,同时也允许后续层“看到”卷积网络输入图像的更大空间范围。卷积网络通常以展平层或全局池化层结束,将空间特征图转换为向量,然后可以通过一系列密集层(MLP)处理以实现分类或回归输出。

很可能常规卷积很快就会被一个等价但更快更高效的替代品所取代(或完全取代):深度可分离卷积(tf.layers.separableConv2d层)。当你从头开始构建一个网络时,强烈推荐使用深度可分离卷积。可分离卷积层可以用作tf.layers .conv2d的即插即用替代品,结果是一个更小更快的网络,在其任务上表现同样好或更好。以下是一个典型的图像分类网络(单标签多类别分类,在本例中)。其拓扑结构包含重复的卷积-池化层组的模式:

const model = tf.sequential();
model.add(tf.layers.separableConv2d({
    filters: 32, kernelSize: 3, activation: 'relu',
    inputShape: [height, width, channels]}));
model.add(tf.layers.separableConv2d({
         filters: 64, kernelSize: 3, activation: 'relu'}));
model.add(tf.layers.maxPooling2d({poolSize: 2}));

model.add(tf.layers.separableConv2d({
         filters: 64, kernelSize: 3, activation: 'relu'}));
model.add(tf.layers.separableConv2d({
         filters: 128, kernelSize: 3, activation: 'relu'}));
model.add(tf.layers.maxPooling2d({poolSize: 2}));

model.add(tf.layers.separableConv2d({
    filters: 64, kernelSize: 3, activation: 'relu'}));
model.add(tf.layers.separableConv2d({
    filters: 128, kernelSize: 3, activation: 'relu'}));
model.add(tf.layers.globalAveragePooling2d());
model.add(tf.layers.dense({units: 32, activation: 'relu'}));
model.add(tf.layers.dense({units: numClasses, activation: 'softmax'}));

model.compile({loss: 'categoricalCrossentropy', optimizer: 'adam'});
循环网络

RNN 通过一次处理一个时间戳的输入序列并始终保持状态来工作。状态通常是一个向量或一组向量(几何空间中的一个点)。在不是时间上不变的序列(例如,时间序列数据,其中最近的过去比遥远的过去更重要)的情况下,应优先使用 RNN,而不是 1D 卷积网络。

TensorFlow.js 提供了三种 RNN 层类型:simpleRNN、GRU 和 LSTM。对于大多数实际目的,您应该使用 GRU 或 LSTM。LSTM 是这两者中更强大的,但也更消耗计算资源。您可以将 GRU 视为 LSTM 的简化和更便宜的替代品。

为了将多个 RNN 层堆叠在一起,除了最后一层之外的每一层都需要配置为返回其输出的完整序列(每个输入时间步对应一个输出时间步)。如果不需要堆叠 RNN 层,通常 RNN 层只需返回最后一个输出,其中包含有关整个序列的信息。

以下是使用单个 RNN 层与密集层一起执行向量序列的二进制分类的示例:

const model = tf.sequential();
model.add(tf.layers.lstm({
  units: 32,
  inputShape: [numTimesteps, numFeatures]
}));
model.add(tf.layers.dense({units: 1, activation: 'sigmoid'}));
model.compile({loss: 'binaryCrossentropy', optimizer: 'rmsprop'});

接下来是一个带有 RNN 层堆叠的模型,用于向量序列的单标签多类别分类:

const model = tf.sequential();
model.add(tf.layers.lstm({
  units: 32,
  returnSequences: true,
  inputShape: [numTimesteps, numFeatures]
}));
model.add(tf.layers.lstm({units: 32, returnSequences: true}));
model.add(tf.layers.lstm({units: 32}));
model.add(tf.layers.dense({units: numClasses, activation: 'softmax'}));
model.compile({loss: 'categoricalCrossentropy', optimizer: 'rmsprop'});
用于减轻过拟合和提高收敛性的层和正则化器

除了上述主要的基本层类型之外,还有一些其他类型的层适用于广泛的模型和问题类型,并协助训练过程。没有这些层,许多机器学习任务的最新准确性不会像今天这样高。例如,dropout 和 batchNormalization 层经常插入到 MLP、卷积网络和 RNN 中,以帮助模型在训练过程中更快地收敛并减少过拟合。以下示例显示了包含 dropout 层的回归 MLP:

const model = tf.sequential();
model.add(tf.layers.dense({
  units: 32,
  activation: 'relu',
  inputShape: [numFeatures]
}));
model.add(tf.layers.dropout({rate: 0.25}));
model.add(tf.layers.dense({units: 64, activation: 'relu'}));
model.add(tf.layers.dropout({rate: 0.25}));
model.add(tf.layers.dense({units: 64, activation: 'relu'}));
model.add(tf.layers.dropout({rate: 0.25}));
model.add(tf.layers.dense({
  units: numClasses,
  activation: 'categoricalCrossentropy'
}));
model.compile({loss: 'categoricalCrossentropy', optimizer: 'rmsprop'});

13.2.3. 从 TensorFlow.js 使用预训练模型

当您要解决的机器学习问题特定于您的应用程序或数据集时,从头开始构建和训练模型是正确的方法,而 TensorFlow.js 使您能够做到这一点。然而,在某些情况下,您面临的问题是通用的,存在预训练模型,这些模型要么完全符合您的要求,要么只需进行轻微调整即可满足您的需求。来自 TensorFlow.js 和第三方开发人员的预训练模型集合。这些模型提供了干净且易于使用的 API。它们还作为 npm 包打包得很好,您可以方便地依赖它们在您的 JavaScript 应用程序(包括 Web 应用程序和 Node.js 项目)中。

在适当的使用案例中使用这些预训练模型可以大大加快您的开发速度。由于不可能列出所有基于 TensorFlow.js 的预训练模型,因此我们只会调查我们所知道的最流行的那些。以@tensorflow-models/为前缀的软件包是由 TensorFlow.js 团队维护的第一方软件包,而其余的是第三方开发者的工作。

@tensorflow-models/mobilenet 是一个轻量级图像分类模型。它能够根据输入图像输出 1,000 个 ImageNet 类别的概率分数。它适用于在网页中为图像标记、从网络摄像头输入流中检测特定内容,以及涉及图像输入的迁移学习任务。虽然@tensorflow-models/mobilenet 关注通用的图像类别,但也有第三方软件包用于更具特定领域的图像分类。例如,nsfwjs 将图像分类为包含色情和其他不当内容与安全内容,这对家长监控、安全浏览等应用非常有用。

如我们在第五章中讨论的那样,目标检测与图像分类不同之处在于,它不仅输出图像中包含的物体,还输出它们在图像坐标系中的位置。@tensorflow-models/coco-ssd 是一个能够检测 90 种对象的目标检测模型。对于每个输入图像,它都能够检测出可能有重叠边界框的多个目标对象(图 13.1,A 面)。

图 13.1。几个使用 TensorFlow.js 构建的预训练 npm 软件包模型的屏幕截图。A 面:@tensorflow-models/coco-ssd 是一个多目标对象检测器。B 面:face-api.js 用于实时人脸和面部特征点检测(通过 Vincent Mühler 的许可从github.com/justadudewhohacks/face-api.js复制)。C 面:handtrack.js 实时跟踪一个或两只手的位置(通过 Victor Dibia 的许可从github.com/victordibia/handtrack.js/复制)。D 面:@tensorflow-models/posenet 使用实时图像输入检测人体的骨骼关键点。E 面:@tensorflow-models/toxicity 检测并标记任何英文文本输入中的七种不当内容。

对于网络应用程序,特定类型的对象特别受到关注,因为它们有可能实现新颖有趣的计算机人类交互。这些包括人脸、手和整个身体。针对每一种类型,都有基于 TensorFlow.js 的专门的第三方模型。对于人脸,face-api.js 和 handsfree 都支持实时人脸跟踪和检测面部特征点(如眼睛或嘴巴;图 13.1,面板 B)。对于手部,handtrack.js 可以实时跟踪一个或两只手的位置(图 13.1,面板 C)。对于整个身体,@tensorflow-models/posenet 实现了高精度、实时的骨架关键点检测(如肩膀、肘部、臀部和膝盖;图 13.1,面板 D)。

对于音频输入模态,@tensorflow-models/speech-commands 提供了一个预训练模型,可以实时检测浏览器的 WebAudio API 中的 18 个英文单词。虽然这不像大词汇连续语音识别那样强大,但它仍然可以在浏览器中实现一系列基于语音的用户交互。

对于文本输入,也有预训练模型可用。例如,来自@tensorflow-models/toxicity 的模型确定给定的英文输入文本在多个维度上的毒性程度(例如,威胁、侮辱或淫秽),这对于辅助内容审核很有用(图 13.1,面板 E)。毒性模型是建立在一个更通用的自然语言处理模型@tensorflow-models/universal-sentence-encoder 之上的,该模型将任何给定的英文句子映射到一个向量,然后可以用于广泛的自然语言处理任务,如意图分类、主题分类、情感分析和问题回答。

需要强调的是,提到的一些模型不仅支持简单的推理,还可以为迁移学习或下游机器学习提供基础,使您能够将这些预训练模型的强大功能应用于您的领域特定数据,而无需进行冗长的模型构建或训练过程。这在一定程度上是由于层和模型的乐高式可组合性。例如,通用句子编码器的输出主要用于下游模型。语音命令模型内置支持您收集新单词类别的语音样本,并基于样本训练一个新的分类器,这对于需要自定义词汇或用户特定语音适应的语音命令应用非常有用。此外,来自 PoseNet 和 face-api.js 等模型的有关头部、手部或身体姿势的时时位置的输出可以输入到一个下游模型中,该模型检测特定的手势或动作序列,这对于许多应用程序非常有用,如辅助使用案例的替代通信。

除了之前提到的面向输入模态的模型之外,还有基于 TensorFlow.js 的第三方预训练模型,面向艺术创造性。例如,ml5.js 包括一个用于图像之间的快速风格转移的模型,以及一个可以自动绘制素描的模型。@magenta/music 提供了一个可以将钢琴音乐转录成谱曲的模型(“音频转谱”),以及一个“旋律的语言模型”,可以根据几个种子音符“写”出旋律,还有其他有趣的预训练模型。

预训练模型的收集庞大且不断增长。JavaScript 社区和深度学习社区都拥有开放的文化和分享精神。随着你在深度学习的旅程中不断前行,你可能会遇到一些有趣且对其他开发者有用的新想法,此时,我们鼓励你将这些模型以我们提到的预训练模型的形式训练、打包并上传到 npm,然后与用户互动并对你的包进行迭代改进。那时,你将真正成为 JavaScript 深度学习社区的一员贡献者。

13.2.4. 可能性的空间

有了所有这些层和预训练模块作为构建模块,你可以构建出哪些有用且有趣的模型呢?记住,构建深度学习模型就像玩乐高积木一样:层和模块可以插在一起,将任何东西映射到任何东西,只要输入和输出表示为张量,并且层具有兼容的输入和输出张量形状。模型的结果层叠执行可微分几何变换,它可以学习输入和输出之间的映射关系,只要关系不过于复杂,以至于超出模型的容量。在这种范式中,可能性的空间是无限的。本节提供了一些示例,以激发你超越我们在本书中强调的基本分类和回归任务的思考。

我们已根据输入和输出模态对建议进行了排序。请注意,其中有不少都在可能性的边界上。虽然模型可以针对任何任务进行训练,只要有足够的训练数据可用,但在某些情况下,这样的模型可能无法很好地泛化远离其训练数据:

  • 将向量映射到向量

    • 预测性医疗保健——将患者医疗记录映射到预测的治疗结果

    • 行为定位——将一组网站属性映射到潜在观众在网站上的行为(包括页面浏览、点击和其他互动)

    • 产品质量控制——将与制造产品相关的一组属性映射到关于产品在市场上表现如何的预测(在市场的不同领域的销售和利润)

  • 将图像映射到向量

    • 医学图像人工智能——将医学图像(如 X 光片)映射到诊断结果

    • 自动车辆转向—将来自摄像头的图像映射到车辆控制信号,如方向盘转向动作

    • 饮食助手—将食物和菜肴的图像映射到预测的健康效应(例如,卡路里计数或过敏警告)

    • 化妆品推荐—将自拍图像映射到推荐的化妆品

  • 将时间序列数据映射到向量

    • 脑机接口—将脑电图(EEG)信号映射到用户意图

    • 行为定向—将产品购买的过去历史(例如电影或书籍购买)映射到未来购买其他产品的概率

    • 地震和余震预测—将地震仪器数据序列映射到地震和随后余震发生的预测概率

  • 将文本映射到向量

    • 电子邮件分类器—将电子邮件内容映射到通用或用户定义的标签(例如,与工作相关的、与家庭相关的和垃圾邮件)

    • 语法评分器—将学生写作样本映射到写作质量评分

    • 基于语音的医疗分诊—将患者对疾病的描述映射到应该转诊给的医疗部门

  • 将文本映射到文本

    • 回复消息建议—将电子邮件映射到一组可能的响应消息

    • 领域特定问答—将客户问题映射到自动回复文本

    • 摘要—将长文章映射到简短摘要

  • 将图像映射到文本

    • 自动生成替代文本—给定一幅图像,生成捕捉内容要点的短文本片段

    • 盲人移动辅助—将内部或外部环境的图像映射到口头指导和有关潜在移动障碍的警告(例如,出口和障碍物位置)

  • 将图像映射到图像

    • 图像超分辨率—将低分辨率图像映射到更高分辨率的图像

    • 基于图像的三维重建—将普通图像映射到同一物体的图像,但从不同角度观察

  • 将图像和时间序列数据映射到向量

    • 医生的多模式助手—将患者的医学图像(例如 MRI)和生命体征历史(血压、心率等)映射到治疗结果的预测
  • 将图像和文本映射到文本

    • 基于图像的问答—将图像和与之相关的问题(例如,一辆二手车的图像和关于其品牌和年份的问题)映射到一个答案
  • 将图像和向量映射到图像

    • 服装和化妆品虚拟试穿—将用户的自拍和化妆品或服装的向量表示映射到用户穿着该产品的图像
  • 将时间序列数据和向量映射到时间序列数据

    • 音乐风格转换—将音乐谱(例如表示为音符时间序列的古典乐曲)和所需风格的描述(例如,爵士乐)映射到所需风格的新音乐谱

正如您可能已经注意到的,此列表中的最后四个类别涉及输入数据中的混合模态。在我们技术史上的这一时刻,生活中的大多数事物都已数字化,因此可以表示为张量,您可以通过深度学习潜在地实现的东西仅受限于您自己的想象力和训练数据的可用性。虽然几乎任何映射都是可能的,但并非每个映射都是。在下一节中,我们将讨论深度学习尚不能做到的事情。

13.2.5。深度学习的限制

可以使用深度学习实现的应用程序空间几乎是无限的。因此,很容易高估深度神经网络的力量,并对它们可以解决的问题过于乐观。本节简要讨论了它们仍然具有的一些限制。

神经网络并不以与人类相同的方式看待世界。

在尝试理解深度学习时,我们面临的一个风险是拟人化,即倾向于误解深度神经网络仿效人类感知和认知。在几个方面,将深度神经网络拟人化是明显错误的。首先,当人类感知到感官刺激(例如带有女孩脸的图像或带有牙刷的图像)时,他们不仅感知到输入的亮度和颜色模式,还提取由这些表面模式表示的更深层次和更重要的概念(例如,年轻女性个体的面孔或口腔卫生产品,以及两者之间的关系)。另一方面,深度神经网络不是这样工作的。当你训练了一个图像标题模型来将图像映射到文本输出时,认为该模型以人类意义理解图像是错误的。在某些情况下,即使是与训练数据中出现的图像类型稍有不同,也可能导致模型生成荒谬的标题(如图 13.2)。

图 13.2。使用深度学习训练的图像标题模型失败

特别是,深度神经网络处理输入的非人类方式被对抗样本突显出来,这些样本是特意设计的,目的是欺骗机器学习模型使其产生分类错误。正如我们在第 7.2 节中通过寻找激活最大的图像来为卷积神经网络滤波器找到的那样,可以在输入空间中进行梯度上升以最大化卷积神经网络滤波器的激活。这个想法可以扩展到输出概率,因此我们可以在输入空间中进行梯度上升,以最大化模型对任何给定输出类别的预测概率。通过给熊猫拍照并添加“长臂猿梯度”,我们可以使模型将图像误分类为长臂猿(图 13.3)。这尽管长臂猿梯度在噪声和幅度上都很小,因此导致的对抗性图像对人类来说看起来与原始熊猫图像无法区分。

图 13.3. 对抗样本:对人眼来说几乎无法察觉的改变可能会影响深度卷积神经网络的分类结果。有关深度神经网络对抗攻击的更多讨论,请参见mng.bz/pyGz

因此,用于计算机视觉的深度神经网络并没有真正理解图像,至少不是以人类的方式。人类学习与深度学习在如何从有限数量的训练样本中泛化这两种学习方式之间存在鲜明对比的另一个领域。深度神经网络可以做到所谓的局部泛化。图 13.4 展示了一个场景,在这个场景中,深度神经网络和人类被要求仅使用少量(比如,八个)训练样本来学习二维参数空间中单个类别的边界。人类意识到类别边界的形状应该是平滑的,区域应该是连通的,并迅速绘制出一个闭合的曲线作为“猜测的”边界。另一方面,神经网络缺乏抽象和先验知识。因此,它可能会得到一个专门的、不规则的边界,严重过拟合于少量训练样本。训练好的模型将在训练样本之外泛化得非常差。增加更多的样本可以帮助神经网络,但这并不总是可行的。主要问题是神经网络是从零开始创建的,只为了解决这个特定的问题。与人类个体不同,它没有任何可以依赖的先验知识,因此不知道要“期望”什么。^([5])这是当前深度学习算法主要局限性的根本原因:通常需要大量的人工标记的训练数据才能训练出一个泛化准确度良好的深度神经网络。

有研究工作在训练单个深度神经网络上进行许多不同且看似无关的任务,以促进跨领域知识共享(参见,例如,Lukasz Kaiser 等人,“学习所有任务的一个模型”,2017 年 6 月 16 日提交,arxiv.org/abs/1706.05137)。但是,这种多任务模型尚未被广泛采用。

图 13.4. 深度学习模型中的局部泛化与人类智能的极端泛化

13.3. 深度学习的趋势

正如我们讨论过的,深度学习在近年取得了惊人的进展,但仍然存在一些局限性。但这个领域并不是静止的;它以惊人的速度不断前进,因此很可能在不久的将来一些局限性会得到改善。本节包含了我们预计在未来几年将见证的深度学习重要突破的一系列合理猜测:

  • 首先,无监督或半监督学习可能会有重大进展。这将对所有形式的深度学习产生深远影响,因为尽管标记数据集的构建成本高昂且难以获得,但在各种业务领域都有大量的未标记数据集。如果我们能够发明一种方法,利用少量标记数据来引导从大量未标记数据中学习,它将为深度学习开启许多新的应用。

  • 其次,深度学习的硬件可能会继续改进,引入越来越强大的神经网络加速器(例如张量处理单元的未来一代^([6]))。这将使研究人员能够使用越来越大的数据集训练更加强大的网络,并继续推动计算机视觉、语音识别、自然语言处理和生成模型等许多机器学习任务的最新技术准确性。

    Norman P. Jouppi 等人,“数据中心张量处理单元™的性能分析”,2017 年,arxiv.org/pdf/1704.04760.pdf

  • 设计模型架构和调整模型超参数可能会变得越来越自动化。我们已经在这个领域看到了一个趋势,如 AutoML^([7]) 和 Google Vizier^([8]) 等技术的示例所示。

    Barret Zoph 和 Quoc V. Le,“利用强化学习进行神经架构搜索”,2016 年 11 月 5 日提交,arxiv.org/abs/1611.01578

    Daniel Golovin,“Google Vizier:一种用于黑盒优化的服务”,2017 年第 23 届 ACM SIGKDD 国际知识发现与数据挖掘会议论文集,第 1487–1495 页,mng.bz/O9yE

  • 神经网络组件的共享和重用可能会继续增长。基于预训练模型的迁移学习将进一步发展。每天都有最先进的深度学习模型变得越来越强大和通用。它们越来越多地在更大更大的数据集上进行训练,有时候为了自动化架构搜索和超参数调整而需要大量的计算资源(请参阅第一和第二条预测)。因此,与其一次又一次地从头开始训练它们,不如对这些预训练模型进行直接推断或迁移学习,这样做更加明智和经济。在某种程度上,这使得深度学习领域更类似于传统的软件工程,高质量的库被定期依赖和重用,这有利于整个领域的标准化和发展速度。

  • 深度学习可能会部署到新的应用领域,改进许多现有解决方案,并开启新的实际应用案例。在我们看来,潜在的应用领域是真正无限的。农业、金融、教育、交通、医疗保健、时尚、体育和娱乐等领域提供了无数等待深度学习从业者探索的机会。

  • 随着深度学习渗透到更多的应用领域,对边缘深度学习的重视可能会日益增加,因为边缘设备最接近用户所在地。因此,该领域可能会发明更小、更节能的神经网络架构,实现与现有更大模型相同的预测准确性和速度。

所有这些预测都将影响 JavaScript 中的深度学习,但最后三个预测尤为重要。可以预期未来 TensorFlow.js 将有更强大、更高效的模型可用。

13.4. 进一步探索的指针

最后,我们想给你一些关于在你翻阅完本书最后一页后如何继续学习和更新知识和技能的指导。尽管现代深度学习领域今天我们所知道的只有几年的历史,但它的漫长而缓慢的前史可以追溯到几十年前。自 2013 年以来,随着财政资源和研究人员数量的指数增长,整个领域现在正以疯狂的速度发展。你在本书中学到的很多东西不会保持很长时间的相关性。深度学习的核心思想(从数据中学习,减少手动特征工程,逐层转换表示)可能会更长时间地存在。更重要的是,通过阅读本书你所建立的知识基础将有望使你能够自己了解深度学习领域的新发展和趋势。幸运的是,这个领域有着开放的文化,其中大多数前沿进展(包括许多数据集!)都以公开可访问和免费的预印本的形式发布,附有公开的博客文章和推文。以下是您应该熟悉的一些顶级资源。

13.4.1. 在 Kaggle 上练习真实世界的机器学习问题

获得机器学习(特别是深度学习)的真实世界经验的有效方法是在 Kaggle 竞赛中尝试手气(kaggle.com)。学习机器学习的唯一真正方法是通过实际的编码、模型构建和调整。这正是本书的哲学,体现在其众多的代码示例中,供您学习、调整和修改。但没有什么比在地基础上使用 TensorFlow.js 等库从头开始构建你的模型和机器学习系统更有效的教你如何做机器学习了。在 Kaggle 上,你可以找到一系列不断更新的数据科学竞赛和数据集,其中许多涉及深度学习。

虽然大多数 Kaggle 用户使用 Python 工具(如 TensorFlow 和 Keras)来解决竞赛问题,但 Kaggle 上的大多数数据集都与语言无关。因此,完全有可能使用非 Python 深度学习框架(如 TensorFlow.js)解决大多数 Kaggle 问题。通过参加一些竞赛,也许作为团队的一部分,你将熟悉本书中描述的一些高级最佳实践的实际应用,尤其是超参数调整和避免验证集过拟合。

13.4.2. 阅读 arXiv 上的最新发展

与其他学术领域相比,深度学习研究几乎完全是公开进行的。论文一经完成并经过审查便会公开并免费提供,许多相关软件也是开源的。ArXiv(arxiv.org)—读作“archive”(X 代表希腊字母chi)—是一家数学、物理和计算机科学论文的开放获取预印本服务器。它成为了发表机器学习和深度学习领域最尖端工作的实际方式,因此也成为了学习该领域最新进展的实际方式。这使得该领域能够以极快的速度前进:所有新的发现和发明都能立即供所有人查阅、评论和建立在其基础之上。

ArXiv 的一个重要缺点是每天发布的新论文数量实在太多,以至于不可能都浏览一遍。ArXiv 上的许多论文没有经过同行评审,这使得它很难识别哪些是重要且高质量的。社区已经建立了工具来应对这些挑战。例如,一个名为 ArXiv Sanity Preserver(arxiv-sanity.com)的网站作为 ArXiv 新论文的推荐引擎,可以帮助您跟踪深度学习特定垂直领域(如自然语言处理或目标检测)的新发展。此外,您还可以使用 Google 学术跟踪您感兴趣的领域和您喜欢的作者的出版物。

13.4.3. 探索 TensorFlow.js 生态系统

TensorFlow.js 拥有充满活力且不断发展的生态系统,包括文档、指南、教程、博客圈和开源项目:

最后的话

这就是JavaScript 深度学习的结尾!希望你在 AI、深度学习以及如何在 JavaScript 中使用 TensorFlow.js 执行一些基本的深度学习任务方面学到了一些东西。像任何有趣且有用的话题一样,学习 AI 和深度学习是一次终身的旅程。这同样适用于将 AI 和深度学习应用于实际问题。无论是专业人士还是业余爱好者都是如此。尽管在深度学习方面取得了许多进展,但大部分基本问题仍然没有得到答案,大部分深度学习的潜在威力也几乎没有得到发掘。请继续学习、质疑、研究、想象、探索、构建和分享!我们期待着看到你用深度学习和 JavaScript 构建的作品!

附录 A:安装 tfjs-node-gpu 及其依赖项

要在 Node.js 中使用 GPU 加速版的 TensorFlow.js(tfjs-node-gpu),你需要在你的计算机上安装 CUDA 和 CuDNN。首先,计算机应配备有支持 CUDA 的 NVIDIA GPU。要检查你的计算机中的 GPU 是否满足该要求,请访问 developer.nvidia.com/cuda-gpus

接下来,我们列出了 Linux 和 Windows 上的驱动程序和库安装的详细步骤,因为这两个操作系统是目前支持 tfjs-node-gpu 的两个操作系统。

A.1. 在 Linux 上安装 tfjs-node-gpu

  1. 我们假设你已经在系统上安装了 Node.js 和 npm,并且 node 和 npm 的路径已包含在你的系统路径中。如果没有,请查看 nodejs.org/en/download/ 获取可下载的安装程序。

  2. developer.nvidia.com/cuda-downloads 下载 CUDA Toolkit。务必选择适合你打算使用的 tfjs-node-gpu 版本的适当版本。在撰写本文时,tfjs-node-gpu 的最新版本为 1.2.10,与 CUDA Toolkit 版本 10.0 兼容。此外,请确保选择正确的操作系统(Linux)、架构(例如,用于主流 Intel CPU 的 x86_64)、Linux 发行版和发行版的版本。你将有下载几种类型安装程序的选项。在这里,我们假设你下载了“runfile(local)”文件(而不是,例如,本地 .deb 包)以供后续步骤使用。

  3. 在你的下载文件夹中,使刚下载的 runfile 可执行。例如,

    chmod +x cuda_10.0.130_410.48_linux.run
    
  4. 使用sudo来运行 runfile。注意,CUDA Toolkit 安装过程可能需要安装或升级 NVIDIA 驱动程序,如果您机器上已安装的 NVIDIA 驱动程序版本过旧或尚未安装此类驱动程序。如果是这种情况,你需要停止 X 服务器,转到仅 shell 模式。在 Ubuntu 和 Debian 发行版中,你可以使用快捷键 Ctrl-Alt-F1 进入仅 shell 模式。按照屏幕上的提示安装 CUDA Toolkit,然后重新启动机器。如果你在仅 shell 模式下,你可以重新启动回到正常的 GUI 模式。

  5. 如果步骤 3 完成正确,nvidia-smi命令现在应该可在你的路径中使用了。你可以使用它来检查 GPU 的状态。它提供了有关安装在你的机器上的 NVIDIA GPU 的名称、温度传感器读数、风扇速度、处理器和内存使用情况,以及当前 NVIDIA 驱动程序版本的信息。当你使用 tfjs-node-gpu 训练深度神经网络时,它是一个方便的实时监视 GPU 的工具。nvidia-smi 的典型输出信息如下(请注意,此机器上有两个 NVIDIA GPU):

    +-----------------------------------------------------------------------------+
    | NVIDIA-SMI 384.111                Driver Version: 384.111                   |
    |-------------------------------+----------------------+----------------------+
    | GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
    | Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
    |===============================+======================+======================|
    |   0  Quadro P1000        Off  | 00000000:65:00.0  On |                  N/A |
    | 41%   53C    P0   ERR! /  N/A |    620MiB /  4035MiB |      0%      Default |
    +-------------------------------+----------------------+----------------------+
    |   1  Quadro M4000        Off  | 00000000:B3:00.0 Off |                  N/A |
    | 46%   30C    P8    11W / 120W |      2MiB /  8121MiB |      0%      Default |
    +-------------------------------+----------------------+----------------------+
    
    +-----------------------------------------------------------------------------+
    | Processes:                                                       GPU Memory |
    |  GPU       PID   Type   Process name                             Usage      |
    |=============================================================================|
    |    0      3876      G   /usr/lib/xorg/Xorg                           283MiB |
    +-----------------------------------------------------------------------------+
    
  6. 将 64 位 CUDA 库文件的路径添加到你的LD_LIBRARY_PATH环境变量中。假设你正在使用 bash shell,你可以将以下行添加到你的 .bashrc 文件中:

    export LD_LIBRARY_PATH="/usr/local/cuda/lib64:${PATH}"
    

    tfjs-node-gpu 在启动时使用LD_LIBRARY_PATH环境变量来找到所需的动态库文件。

  7. developer.nvidia.com/cudnn下载 CuDNN。为什么除了 CUDA 还需要 CuDNN 呢?这是因为 CUDA 是一个通用的计算库,除了深度学习之外还可以应用在其他领域(例如流体力学)。CuDNN 是 NVIDIA 基于 CUDA 构建的加速深度神经网络操作的库。NVIDIA 可能要求你创建一个登录账号并回答一些调查问题才能下载 CuDNN。一定要下载与之前步骤安装的 CUDA Toolkit 版本相匹配的 CuDNN 版本。例如,CuDNN 7.6 与 CUDA Toolkit 10.0 一起使用。

  8. 与 CUDA Toolkit 不同,下载的 CuDNN 没有可执行安装程序。相反,它是一个压缩的 tarball,其中包含了一些动态库文件和 C/C++头文件。这些文件应该被提取并复制到适当的目标文件夹中。你可以使用如下的一系列命令来实现这一点:

    tar xzvf cudnn-10.0-linux-x64-v7.6.4.38.tgz
    cp cuda/lib64/* /usr/local/cuda/lib64
    cp cuda/include/* /usr/local/cuda/include
    
  9. 现在,所有必需的驱动程序和库都已安装完成,你可以通过在 node 中导入 tfjs-node-gpu 来快速验证 CUDA 和 CuDNN:

    npm i @tensorflow/tfjs @tensorflow/tfjs-node-gpu
    node
    

    然后,在 Node.js 命令行界面上,

    > const tf = require('@tensorflow/tfjs');
    > require('@tensorflow/tfjs-node-gpu');
    

    如果一切顺利,你应该会看到一系列日志行,确认发现了一个(或多个,取决于你的系统配置)可以被 tfjs-node-gpu 使用的 GPU:

    2018-09-04 13:08:17.602543: I
    tensorflow/core/common_runtime/gpu/gpu_device.cc:1405] Found device 0
    with properties:
     name: Quadro M4000 major: 5 minor: 2 memoryClockRate(GHz): 0.7725
     pciBusID: 0000:b3:00.0
     totalMemory: 7.93GiB freeMemory: 7.86GiB
     2018-09-04 13:08:17.602571: I
    tensorflow/core/common_runtime/gpu/gpu_device.cc:1484] Adding visible
    gpu devices: 0
     2018-09-04 13:08:18.157029: I
    tensorflow/core/common_runtime/gpu/gpu_device.cc:965] Device
    interconnect StreamExecutor with strength 1 edge matrix:
     2018-09-04 13:08:18.157054: I
    tensorflow/core/common_runtime/gpu/gpu_device.cc:971]      0
     2018-09-04 13:08:18.157061: I
    tensorflow/core/common_runtime/gpu/gpu_device.cc:984] 0:   N
     2018-09-04 13:08:18.157213: I
    tensorflow/core/common_runtime/gpu/gpu_device.cc:1097] Created
    TensorFlow device (/job:localhost/replica:0/task:0/device:GPU:0 with
    7584 MB memory) -> physical GPU (device: 0, name: Quadro M4000, pci bus
    id: 0000:b3:00.0, compute capability: 5.2)
    
  10. 现在,你已经准备好使用 tfjs-node-gpu 的所有功能了。只需确保在你的 package.json 中包含以下依赖项(或其后续版本):

      ...
      "dependencies": {
        "@tensorflow/tfjs": "⁰.12.6",
        "@tensorflow/tfjs-node": "⁰.1.14",
        ...
      }
      ...
    

    在你的主要的 .js 文件中,确保导入基本的依赖项,包括@tensorflow/tfjs@tensorflow/tfjs-node-gpu。前者给你提供了 TensorFlow.js 的通用 API,而后者将 TensorFlow.js 操作与基于 CUDA 和 CuDNN 实现的高性能计算内核相连:

    const tf = require('@tensorflow/tfjs');
    require('@tensorflow/tfjs-node-gpu');
    

A.2. 在 Windows 上安装 tfjs-node-gpu

  1. 确保您的 Windows 符合 CUDA Toolkit 的系统要求。某些 Windows 版本和 32 位机器架构不受 CUDA Toolkit 的支持。有关更多详情,请参阅 docs.nvidia.com/cuda/cuda-installation-guide-microsoft-windows/index.html#system-requirements

  2. 我们假设您已在系统上安装了 Node.js 和 npm,并且 Node.js 和 npm 的路径在您系统的环境变量 Path 中可用。如果没有,请访问 nodejs.org/en/download/ 下载安装程序。

  3. 安装 Microsoft Visual Studio,因为它是安装 CUDA Toolkit 所必需的。请参阅步骤 1 中相同的链接,以了解应安装哪个版本的 Visual Studio。

  4. 下载并安装 Windows 版 CUDA Toolkit。在撰写本文时,运行 tfjs-node-gpu 需要 CUDA 10.0(最新版本:1.2.10)。请务必为您的 Windows 发行版选择正确的安装程序。支持 Windows 7 和 Windows 10 的安装程序。此步骤需要管理员权限。

  5. 下载 CuDNN。确保 CuDNN 的版本与 CUDA 的版本匹配。例如,CuDNN 7.6 与 CUDA Toolkit 10.0 匹配。在下载 CuDNN 之前,NVIDIA 可能要求您在其网站上创建帐户并回答一些调查问题。

  6. 与 CUDA Toolkit 安装程序不同,您刚下载的 CuDNN 是一个压缩文件。解压它,您将看到其中有三个文件夹:cuda/bin、cuda/include 和 cuda/lib/x64。找到 CUDA Toolkit 安装的目录(默认情况下,它类似于 C:/Program Files/NVIDIA CUDA Toolkit 10.0/cuda)。将解压后的文件复制到那里相应名称的子文件夹中。例如,解压的 zip 存档中的 cuda/bin 中的文件应复制到 C:/Program Files/NVIDIA CUDA Toolkit 10.0/cuda/bin。此步骤可能还需要管理员权限。

  7. 安装 CUDA Toolkit 和 CuDNN 后,请重新启动 Windows 系统。我们发现这对于所有新安装的库都能正确加载以供 tfjs-node-gpu 使用是必要的。

  8. 安装 npm 包 window-build-tools。这是下一步安装 npm 包 @tensorflow/tfjs-node-gpu 所必需的:

    npm install --add-python-to-path='true' --global windows-build-tools
    
  9. 使用 npm 安装包 @tensorflow/tfjs@tensorflow/tfjs-node-gpu

    npm -i @tensorflow/tfjs @tensorflow/tfjs-node-gpu
    
  10. 要验证安装是否成功,请打开节点命令行并运行

    > const tf = require('@tensorflow/tfjs');
    > require('@tensorflow/tfjs-node-gpu');
    

    确保这两个命令都能顺利完成。在第二个命令之后,您应该在控制台中看到一些由 TensorFlow GPU 共享库打印的日志行。这些行将列出 tfjs-node-gpu 已识别并将在后续深度学习程序中使用的 CUDA 启用的 GPU 的详细信息。

附录 B:TensorFlow.js 中张量和操作的快速教程

本附录重点介绍 TensorFlow.js API 中与非tf.Model部分有关的内容。虽然tf.Model提供了一套完整的方法来训练和评估模型,并将其用于推理,但通常需要使用 TensorFlow.js 中的非tf.Model部分来处理tf.Model对象。最常见的情况是

  • 将您的数据转换为可供tf.Model对象输入的张量

  • tf.Model所做的预测数据转换为张量格式,以便其他程序部分可以使用

正如您将看到的,将数据放入和取出张量并不困难,但有一些传统模式和值得注意的点值得指出。

B.1. 张量的创建和张量轴约定

请记住,张量只是一个数据容器。每个张量都有两个基本属性:数据类型(dtype)和形状。dtype 控制张量中存储的值的类型。给定张量只能存储一种类型的值。截至本文撰写时(版本 0.13.5),支持的 dtype 为 float32、int32 和 bool。

形状是一个整数数组,指示张量中有多少个元素以及它们是如何组织的。可以将其视为张量的“形状和大小”,即张量作为容器的形状(参见 图 B.1)。

图 B.1. 秩为 0、1、2、3 和 4 的张量示例

形状的长度被称为张量的。例如,1D 张量,也称为向量,秩为 1。1D 张量的形状是一个包含一个数字的数组,这个数字告诉我们 1D 张量的长度。将秩增加一,我们得到一个 2D 张量,可以将其可视化为 2D 平面上的数字网格(如灰度图像)。2D 张量的形状有两个数字,告诉我们网格的高度和宽度。再增加一秩,我们得到一个 3D 张量。如 图 B.1 中的示例所示,你可以将 3D 张量可视化为 3D 数字网格。3D 张量的形状由三个整数组成;它们告诉我们沿着三个维度的 3D 网格的大小。所以,你看到了规律。秩为 4 的张量(4D 张量)更难以直接可视化,因为我们生活的世界只有三个空间维度。4D 张量经常在许多模型中使用,例如深度卷积网络。TensorFlow.js 支持秩高达 6 的张量。在实践中,秩为 5 的张量仅在某些小众情况下使用(例如涉及视频数据的情况),而秩为 6 的张量甚至更少见。

B.1.1. 标量(秩为 0 的张量)

标量是形状为空数组([])的张量。它没有轴,始终只包含一个值。可以使用 tf.scalar() 函数创建一个新的标量。在 JavaScript 控制台(假设已加载 TensorFlow.js 并在 tf 符号处可用)中执行以下操作:

> const myScalar = tf.scalar(2018);[1]
> myScalar.print();
Tensor
    2018
> myScalar.dtype;
"float32"
> myScalar.shape;
[]
> myScalar.rank;
0

¹

请注意,出于空间和清晰起见,我们将跳过由于赋值而产生的 JavaScript 控制台输出行,因为它们对所讨论的问题没有说明性。

我们已创建一个标量张量,其中仅包含值 2018。其形状是空列表,正如预期的那样。它具有默认的 dtype ("float32")。要将 dtype 强制为整数,请在调用 tf.scalar() 时提供 'int32' 作为额外参数:

> const myIntegerScalar = tf.scalar(2018, 'int32');
> myIntegerScalar.dtype;
"int32"

要从张量中获取数据,我们可以使用异步方法data()。该方法是异步的,因为一般来说,张量可能被托管在主内存之外,例如在 GPU 上,作为 WebGL 纹理。检索这些张量的值涉及到不一定能立即解决的操作,我们不希望这些操作阻塞主 JavaScript 线程。这就是为什么data()方法是异步的。还有一个同步函数通过轮询检索张量的值:dataSync()。这种方法很方便,但会阻塞主 JavaScript 线程,所以应该尽量少用(例如,在调试期间)。尽量使用异步的data()方法:

> arr = await myScalar.data();
Float32Array [2018]
> arr.length
1
> arr[0]
2018

要使用dataSync()

> arr = myScalar.dataSync();
Float32Array [2018]
> arr.length
1
> arr[0]
2018

我们看到,对于 float32 类型的张量,data()dataSync()方法将值作为 JavaScript 的Float32Array原始值返回。如果你期望的是一个普通的数字,这可能有点令人惊讶,但是当考虑到其他形状的张量可能需要返回包含多个数字的容器时,这就更合理了。对于 int32 类型和 bool 类型的张量,data()dataSync()分别返回Int32ArrayUint8Array

请注意,即使标量始终包含一个元素,反之则不成立。张量的秩大于 0 的张量也可以只有一个元素,只要其形状中的数字乘积为 1 即可。例如,形状为[1, 1]的 2D 张量只有一个元素,但是它有两个轴。

B.1.2. tensor1d(秩-1 张量)

1D 张量有时被称为秩-1 张量或向量。1D 张量恰好有一个轴,其形状是长度为 1 的数组。下面的代码将在控制台创建一个向量:

> const myVector = tf.tensor1d([-1.2, 0, 19, 78]);
> myVector.shape;
[4]
> myVector.rank;
1
> await  myVector.data();
Float32Array(4) [-1.2, 0, 19, 78]

这个 1D 张量有四个元素,可以称为 4 维向量。不要混淆 4D向量和 4D张量!4D 向量是一个只有一个轴并且包含确切四个值的 1D 张量,而 4D 张量有四个轴(并且每个轴上可以有任意数量的维度)。维度可以表示沿着特定轴的元素数量(如我们的 4D 向量)或张量中的轴数(例如,4D 张量),这有时可能会令人困惑。在技术上,更正确和不含糊的是指一个秩-4 张量,但是无论如何都常见到模糊的表示法 4D 张量。在大多数情况下,这不应该是个问题,因为它可以根据上下文来消除歧义。

与标量张量一样,您可以使用data()dataSync()方法来访问 1D 张量元素的值;例如,

> await myVector.data()
Float32Array(4) [-1.2000000476837158, 0, 19, 78]

或者,您可以使用data()的同步版本,即dataSync(),但要注意,如果可能,应该避免使用dataSync()会阻塞 UI 线程:

> myVector.dataSync()
Float32Array(4) [-1.2000000476837158, 0, 19, 78]

要访问 1D 张量的特定元素的值,您可以简单地索引到data()dataSync()返回的 TypedArray;例如,

> [await myVector.data()][2]
19

B.1.3. tensor2d(秩-2 张量)

一个二维张量有两个轴。在某些情况下,一个二维张量被称为矩阵,它的两个轴可以被解释为矩阵的行和列索引,分别。您可以将矩阵视为元素的矩形网格(参见图 B.1 的第三面板)。在 TensorFlow.js 中,

> const myMatrix = tf.tensor2d([[1, 2, 3], [40, 50, 60]]);
> myMatrix.shape;
[2, 3]
> myMatrix.rank;
2

第一个轴的条目称为,第二个轴的条目称为。在前面的例子中,[1, 2, 3] 是第一行,[1, 40] 是第一列。重要的是要知道,当使用 data()dataSync() 返回数据时,数据将以行优先的方式作为扁平数组返回。换句话说,第一行的元素将首先出现在 Float32Array 中,然后是第二行的元素,依此类推:^([2])

²

这与一些其他数值框架(如 MATLAB 和 R)中看到的列优先排序不同。

> await myMatrix.data();
Float32Array(6) [1, 2, 3, 40, 50, 60]

之前,我们提到 data()dataSync() 方法,当跟随索引时,可以用于访问一维张量的任何元素的值。当用于二维张量时,索引操作变得繁琐,因为 data()dataSync() 返回的 TypedArray 会扁平化二维张量的元素。例如,为了确定与二维张量中第二行第二列的元素对应的 TypedArray 元素,您必须执行如下算术:

> (await myMatrix.data())[1 * 3 + 1];
50

幸运的是,TensorFlow.js 提供了另一组方法,用于将张量的值下载到普通的 JavaScript 数据结构中:array()arraySync()。与 data()dataSync() 不同,这些方法返回正确保留原始张量秩和形状的嵌套 JavaScript 数组。例如,

> JSON.stringify(await myMatrix.array())
 "[[1,2,3],[40,50,60]]"

要访问第二行第二列的元素,我们只需对嵌套数组进行两次索引:

> (await myMatrix.array())[1][1]
 50

这消除了执行索引算术的需要,并且对于更高维度的张量特别方便。arraySync()array() 的同步版本。与 dataSync() 类似,arraySync() 可能会阻塞 UI 线程,应谨慎使用。

tf.tensor2d() 调用中,我们提供了一个嵌套的 JavaScript 数组作为参数。参数由嵌套在另一个数组中的数组行组成。这种嵌套结构由 tf.tensor2d() 用于推断二维张量的形状——即有多少行和多少列,分别。使用 tf.tensor2d() 创建相同的二维张量的另一种方法是提供元素作为平面(非嵌套)JavaScript 数组,并伴随一个第二个参数,指定二维张量的形状:

> const myMatrix = tf.tensor2d([1, 2, 3, 40, 50, 60], [2, 3]);
> myMatrix.shape;
[2, 3]
> myMatrix.rank;
2

在这种方法中,shape 参数中所有数字的乘积必须与浮点数组中的元素数相匹配,否则在 tf.tensor2d() 调用期间将抛出错误。对于秩高于 2 的张量,创建张量还有两种类似的方法:使用一个嵌套数组作为参数,或者使用一个带有形状参数的平坦数组。在本书的不同示例中,您会看到这两种方法都被使用。

B.1.4. 秩为 3 及更高维度的张量

如果您将几个 2D 张量打包到一个新数组中,您将获得一个 3D 张量,您可以将其想象为元素的立方体(图 B.1 中的第四个面板)。在 TensorFlow.js 中,可以按照以前的模式创建秩为 3 的张量:

> const myRank3Tensor = tf.tensor3d([[[1, 2, 3],
                                      [4, 5, 6]],
                                      [[10, 20, 30],
                                      [40, 50, 60]]]);
> myRank3Tensor.shape;
[2, 2, 3]
> myRank3Tensor.rank;
3

另一种执行相同操作的方法是提供一个扁平(非嵌套)值数组,以及一个显式形状:

> const anotherRank3Tensor = tf.tensor3d(
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
    [2, 2, 3]);

在这个例子中,tf.tensor3d() 函数可以被更通用的 tf.tensor() 函数替代。这允许你生成任何秩(rank)的张量,最高可达到 6。在下面的示例中,我们创建了一个秩为 3 和一个秩为 6 的张量:

> anotherRank3Tensor = tf.tensor(
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
    [2, 2, 3]);
> anotherRank3Tensor.shape;
[2, 2, 3]
> anotherRank3Tensor.rank;
3

> tinyRank6Tensor = tf.tensor([13], [1, 1, 1, 1, 1, 1]);
> tinyRank6Tensor.shape;
[1, 1, 1, 1, 1, 1]
> tinyRank6Tensor.rank;
6

B.1.5. 数据批次的概念

在实践中,在深度学习中你将遇到的所有张量中,第一个轴(轴 0,因为索引从 0 开始)几乎总是批处理轴(有时称为样本轴批处理维度)。因此,一个模型作为输入获取的实际张量的秩比单个输入特征的秩高 1。这一点贯穿于本书中所有 TensorFlow.js 模型。第一个维度的大小等于批次中的示例数,称为批处理大小。例如,在第三章(清单 3.9)中的鸢尾花分类示例中,每个示例的输入特征由表示为长度为 4 的向量的四个数字组成(形状为 [4] 的 1D 张量)。因此,鸢尾花分类模型的输入是 2D 的,形状为 [null, 4],其中第一个 null 值表示模型运行时将确定的批处理大小(见图 B.2)。这种批处理约定也适用于模型的输出。例如,鸢尾花分类模型为每个个别输入示例输出一个三种可能的鸢尾花类型的独热编码,这是一个形状为 [3] 的 1D 张量。但是,模型的实际输出形状是 2D 的,形状为 [null, 3],其中第一个 null 值是待确定的批处理大小。

图 B.2. 单个示例(左)和批示例(右)的张量形状。批示例的张量比单个示例的张量的秩高一级,并且是tf.Model对象的predict()fit()evaluate()方法所接受的格式。批示例张量的形状中的null表示张量的第一维具有一个未确定的大小,在对上述方法进行实际调用时可以是任何正整数。

B.1.6. 张量的真实例子

让我们通过几个与本书中遇到的类似的例子使张量更加具体化。你将操作的数据几乎总是属于以下类别之一。在前面的讨论中,我们遵循批处理约定,并始终将批次中的示例数(numExamples)作为第一个轴包括进去:

  • 向量数据—形状为[numExamples, features]的 2D 张量

  • 时间序列(序列)数据—形状为[numExamples, timesteps, features]的 3D 张量

  • 图像—形状为[numExamples, height, width, channels]的 4D 张量

  • 视频—形状为[numExamples, frame, height, width, channels]的 5D 张量

向量数据

这是最常见的情况。在这样的数据集中,每个单独的数据样本可以被编码为一个向量,因此数据的批将被编码为一个秩为 2 的张量,其中第一个轴是样本轴,第二个轴是特征轴。

让我们看两个例子:

  • 一个人口数据集,其中我们考虑每个人的年龄、邮政编码和收入。每个人可以被描述为一个包含 3 个值的向量,因此一个包含 10 万人的完整数据集可以存储在形状为[100000, 3]的 2D 张量中。

  • 一个文本文档数据集,其中我们通过每个单词出现的次数来表示每个文档(例如,从包含 20000 个最常见单词的英语词典中)。每个文档可以被编码为一个包含 20000 个值的向量(词典中每个单词的计数),因此 500 个文档的批可以存储在形状为[500, 20000]的张量中。

时间序列或序列数据

每当数据中涉及时间(或者序列顺序的概念)时,将其存储在具有显式时间轴的 3D 张量中是有意义的。每个样本被编码为一系列向量(一个 2D 张量),因此样本批将被编码为 3D 张量(参见图 B.3)。

图 B.3. 一个 3D 时间序列数据张量

几乎总是按照惯例,时间轴是第二个轴(索引为 1 的轴),如下例所示:

  • 一个股票价格数据集。每分钟我们存储股票的当前价格,过去一分钟内的最高价格和最低价格。因此,每分钟被编码为一个三个值的向量。由于一个小时有 60 分钟,一小时的交易被编码为一个形状为 [60, 3] 的二维张量。如果我们有一个包含 250 个独立小时序列的数据集,数据集的形状将是 [250, 60, 3]

  • 一个推文数据集,其中我们将每个推文编码为由 128 个唯一字符组成的 280 个字符序列。在这种设置中,每个字符都可以编码为大小为 128 的二进制向量(除了在对应字符的索引处有一个 1 的条目外,全部为零)。然后,每个字符可以被视为一个形状为 [280, 128] 的二阶张量。一个包含 100 万条推文的数据集可以存储在一个形状为 [1000000, 280, 128] 的张量中。

图像数据

图像的数据通常具有三个维度:高度、宽度和颜色深度。尽管灰度图像只有一个颜色通道,但按照惯例,图像张量始终是三阶的,对于灰度图像有一个一维的颜色通道。因此,一个大小为 256 × 256 的 128 个灰度图像批次将存储在一个形状为 [128, 256, 256, 1] 的张量中,而一个包含 128 个彩色图像的批次将存储在一个形状为 [128, 256, 256, 3] 的张量中(参见图 B.4)。这称为 NHWC 约定(有关更多详情,请参见第四章)。

图 B.4. 一个 4D 图像数据张量

一些框架在高度和宽度之前放置通道维度,使用 NCHW 约定。在本书中,我们不使用这个约定,但在其他地方看到形状如 [128, 3, 256, 256] 的图像张量也不要感到惊讶。

视频数据

原始视频数据是少数几种常见的真实世界数据之一,你需要使用到五维张量。视频可以理解为一系列帧,每一帧都是一幅彩色图像。由于每一帧可以存储在一个三维张量 [height, width, colorChannel] 中,一系列帧可以存储在一个四维张量 [frames, height, width, colorChannel] 中,因此一批不同的视频将存储在一个五维张量中,形状为 [samples, frames, height, width, colorChannel]

例如,一个 60 秒长,144 × 256 分辨率的 YouTube 视频剪辑,每秒采样 4 帧,将有 240 帧。四个这样的视频剪辑批次将存储在一个形状为 [4, 240, 144, 256, 3] 的张量中。总共有 106,168,320 个值!如果张量的数据类型为 'float32',那么每个值将以 32 位存储,因此张量将表示 405 MB。这是大量的数据!你在现实生活中遇到的视频要轻得多,因为它们不是以 float32 存储,并且通常被大幅压缩(例如 MPEG 格式)。

B.1.7. 从张量缓冲区创建张量

我们已经展示了如何使用诸如 tf.tensor2d()tf.tensor() 等函数从 JavaScript 数组创建张量。为此,您必须先确定所有元素的值,并在之前的 JavaScript 数组中设置它们。但是,在某些情况下,从头开始创建这样一个 JavaScript 数组有点繁琐。例如,假设您要创建一个 5 × 5 矩阵,其中所有的非对角线元素都为零,而对角线元素形成一个递增序列,等于行或列索引加 1:

[[1, 0, 0, 0, 0],
 [0, 2, 0, 0, 0],
 [0, 0, 3, 0, 0],
 [0, 0, 0, 4, 0],
 [0, 0, 0, 0, 5]]

如果要创建一个满足此要求的嵌套 JavaScript 数组,代码将如下所示:

const n = 5;
const matrixArray = [];
for (let i = 0; i < 5; ++i) {
  const row = [];
  for (let j = 0; j < 5; ++j) {
    row.push(j === i ? i + 1 : 0);
  }
  matrixArray.push(row);
}

最后,你可以把嵌套的 JavaScript 数组 matrixArray 转换成一个二维张量:

> const matrix = tf.tensor2d(matrixArray);

这段代码看起来有点繁琐。它涉及两个嵌套的 for 循环。有没有简化它的方法?答案是有:我们可以使用 tf.tensorBuffer() 方法创建一个 TensorBufferTensorBuffer 对象允许您通过索引指定其元素,并使用 set() 方法更改其值。这与 TensorFlow.js 中的张量对象不同,后者的元素值是不可变的。当您完成设置 TensorBuffer 的所有元素的值后,可以通过其 toTensor() 方法方便地将 TensorBuffer 转换为实际的张量对象。因此,如果我们使用 tf.tensorBuffer() 来实现与上述代码相同的张量创建任务,新代码将如下所示

const buffer = tf.tensorBuffer([5, 5]);   ***1***
for (let i = 0; i < 5; ++i) {
  buffer.set(i +  1, i, i);               ***2***
}
const matrix = buffer.toTensor();         ***3***
  • 1 创建 TensorBuffer 时指定张量形状。创建后,TensorBuffer 的所有值都为零。

  • 2 第一个参数是所需的值,而其余的参数是要设置的元素的索引。

  • 3 从 TensorBuffer 获取实际的张量对象

因此,通过使用 tf.tensorBuffer(),我们将代码行数从 10 减少到 5。

B.1.8. 创建全零和全一张量

通常希望创建一个给定形状的全零张量。你可以使用 tf.zeros() 函数来实现这一点。调用该函数时,将期望的形状作为输入参数提供;例如,

> const x = tf.zeros([2, 3, 3]);
> x.print();
Tensor
    [[[0, 0, 0],
      [0, 0, 0],
      [0, 0, 0]],
      [[0, 0, 0],
      [0, 0, 0],
      [0, 0, 0]]]

创建的张量具有默认的 dtype(float32)。要创建其他 dtype 的全零张量,请将 dtype 指定为 tf.zeros() 的第二个参数。

相关的函数是 tf.zerosLike(),它让您可以创建一个与现有张量具有相同形状和 dtype 的全零张量。例如,

> const y = tf.zerosLike(x);

等同于

> const y = tf.zeros(x.shape, x.dtype);

但更简洁。

类似的方法允许你创建所有元素都相等的张量:tf.ones()tf.onesLike()

B.1.9. 创建随机值张量

创建具有随机值的张量在许多情况下都很有用,比如权重的初始化。创建具有随机值张量最常用的函数是 tf.randomNormal()tf.randomUniform()。这两个函数具有类似的语法,但导致元素值的分布不同。顾名思义,tf.randomNormal() 返回的张量中的元素值遵循正态(高斯)分布。如果你只用一个形状参数调用该函数,你会得到一个元素遵循 单位 正态分布的张量:均值 = 0,标准差(SD)= 1。例如,

³

对于熟悉统计学的读者,元素值彼此独立。

> const x = tf.randomNormal([2, 3]);
> x.print():
Tensor
    [[-0.2772508, 0.63506  , 0.3080665],
     [0.7655841 , 2.5264773, 1.142776 ]]

如果你希望正态分布有一个非默认的均值或标准差,你可以将它们作为第二和第三个输入参数提供给 tf.randomUniform()。例如,以下调用创建了一个元素值遵循均值 = -20,标准差 = 0.6 的正态分布的张量:

> const x = tf.randomNormal([2, 3], -20, 0.6);
> x.print();
Tensor
    [[-19.0392246, -21.2259483, -21.2892818],
     [-20.6935596, -20.3722878, -20.1997948]]

tf.randomUniform() 允许你创建具有均匀分布元素值的随机张量。默认情况下,均匀分布是单位分布,即下界为 0,上界为 1:

> const x = tf.randomUniform([3, 3]);
> x.print();
Tensor
    [[0.8303654, 0.3996494, 0.3808384],
     [0.0751046, 0.4425731, 0.2357403],
     [0.4682371, 0.0980235, 0.7004037]]

如果你想让元素值遵循非单位均匀分布,你可以将下界和上界指定为 tf.randomUniform() 的第二和第三个参数,例如,

> const x = tf.randomUniform([3, 3], -10, 10);

创建一个值在 [-10, 10) 区间内随机分布的张量:

> x.print();
Tensor
    [[-7.4774652, -4.3274679, 5.5345411 ],
     [-6.767087 , -3.8834026, -3.2619202],
     [-8.0232048, 7.0986223 , -1.3350322]]

tf.randomUniform() 可用于创建具有随机值的 int32 类型张量。这在你想要生成随机标签的情况下非常有用。例如,以下代码创建了一个长度为 10 的向量,其中的值随机抽取自整数 0 到 100(区间 [0, 100)):

> const x = tf.randomUniform([10], 0, 100, 'int32');
> x.print();
Tensor
    [92, 16, 65, 60, 62, 16, 77, 24, 2, 66]

注意,在这个例子中 'int32' 参数是关键。没有它,你得到的张量将包含 float32 值而不是 int32 值。

B.2.基础张量操作

如果我们无法在张量上执行操作,那么张量就不会有什么用处。TensorFlow.js 支持大量的张量操作。您可以在 js.tensorflow.org/api/latest 查看它们的列表以及它们的文档。描述每一个操作都会很枯燥而冗余。因此,我们将突出一些常用的操作作为示例。常用操作可以分为两种类型:一元和二元。一元运算以一个张量作为输入并返回一个新张量,而二元运算以两个张量作为输入并返回一个新张量。

B.2.1.一元操作

让我们考虑将张量取反的操作——即使用每个输入张量元素的负值——并形成一个具有相同形状和数据类型的新张量。这可以使用 tf.neg() 完成:

> const x = tf.tensor1d([-1, 3, 7]);
> const y = tf.neg(x);
> y.print();
Tensor
    [1, -3, -7]
函数式 API 与链式 API

在上一个示例中,我们使用张量x作为输入参数调用函数tf.neg()。TensorFlow.js 提供了一种更简洁的执行数学等价操作的方法:使用张量对象本身的neg()方法,而不是tf.*命名空间下的函数:

> const y = x.neg();

在这个简单的例子中,由于新 API 的存在,由于键入次数较少而节省的打字量可能不会显得那么令人印象深刻。然而,在需要一个接一个地应用多个操作的情况下,第二个 API 将比第一个 API 表现出更大的优势。例如,考虑一个假设的算法,您想要将x取反,计算倒数(每个元素都被 1 除),并在上面应用relu激活函数。这是在第一个 API 中实现算法所需的代码:

> const y = tf.relu(tf.reciprocal(tf.neg(x)));

相比之下,在第二个 API 中,实现代码如下:

> const y = x.neg().reciprocal().relu();

第二种实现在以下几个方面优于第一种实现:

  • 字符较少,输入更少,因此制造错误的机会更小。

  • 没有必要平衡嵌套的开放和关闭括号(尽管大多数现代代码编辑器都会帮助您完成此操作)。

  • 更重要的是,方法出现在代码中的顺序与底层数学操作发生的顺序相匹配。(注意在第一种实现中,顺序被颠倒了。)这在第二种实现中会导致更好的代码可读性。

我们将第一个 API 称为函数式API,因为它基于在tf.命名空间下调用函数。第二个 API 将称为链式API,因为操作按照链式顺序出现(正如您在前面的示例中所看到的)。在 TensorFlow.js 中,大多数操作都可以作为tf.*命名空间下的函数版本和作为张量对象方法的链式版本来访问。您可以根据自己的需求选择这两个 API。在本书中,我们在不同的地方同时使用这两个 API,但对于涉及连续操作的情况,我们更偏向于使用链式 API。

元素级与约减操作

我们提到的一元操作的示例(tf.neg()tf.reciprocal()tf.relu())具有一个共同特点,即操作独立地应用在输入张量的每个元素上。因此,这类操作返回的张量保留了输入张量的形状。然而,在 TensorFlow.js 中,其他一元操作会导致张量形状比原来的更小。在张量形状的背景下,"更小" 是什么意思呢?在某些情况下,它表示较低的秩。例如,一元操作可能返回一个标量(秩为 0 的张量),而原来的张量是一个 3D 张量(秩为 3)。在其他情况下,它表示某个维度的大小比原来的更小。例如,一元操作可能对一个形状为[3, 20]的输入返回一个形状为[3, 1]的张量。无论形状如何收缩,这些操作都被称为约减操作

tf.mean()是最常用的约减操作之一。在链式 API 中,它作为Tensor类的mean()方法出现。当没有附加参数时,它计算输入张量的所有元素的算术平均值,而无论其形状如何,并返回一个标量。在链式 API 中,使用它的方式如下所示:

> const x = tf.tensor2d([[0, 10], [20, 30]]);
> x.mean().print();
Tensor
    15

有时,我们需要单独计算 2D 张量(矩阵)的每一行的均值,而不是整个张量上的均值。可以通过向mean()方法提供附加参数来实现:

> x.mean(-1).print();
Tensor
    [5, 25]

参数-1表示mean()方法应该计算张量的最后一个维度的算术平均值。([4]) 这个维度被称为约减维度,因为它将在输出张量中被“减少”,输出张量的秩变为 1。指定约减维度的另一种方式是使用实际的维度索引:

这遵循了 Python 的索引约定。

> x.mean(1).print();

请注意,mean()还支持多个约减维度。例如,如果您有一个形状为[10, 6, 3]的 3D 张量,并且希望计算其算术平均值在最后两个维度上进行计算,得到一个形状为[10]的 1D 张量,则可以调用mean()方法,如x.mean([-2, -1])x.mean([1, 2])。我们在附录的最后给出这个方法作为一个练习。

其他经常使用的约简一元操作包括

  • tf.sum() 几乎与 tf.mean() 相同,但它计算的是和,而不是算术平均值,而是元素。

  • tf.norm(),用于计算元素的范数。有不同类型的范数。例如,1-范数是元素绝对值的总和。2-范数通过对平方元素求和然后取平方根来计算。换句话说,它是欧几里德空间中向量的长度。tf.norm()可用于计算一组数字的方差或标准差。

  • tf.min()tf.max(),分别计算元素的最小值和最大值。

  • tf.argMax() 返回沿减少轴的最大元素的索引。此操作经常用于将分类模型的概率输出转换为获胜类别的索引(例如,请参阅第 3.3.2 节中的鸢尾花分类问题)。tf.argMin() 提供了类似的功能以找到最小值。

我们提到逐元素操作保留输入张量的形状。但反之不成立。有些保持形状的操作不是逐元素的。例如,tf.transpose() 操作可以执行矩阵转置,其中输入 2D 张量中的索引 [i, j] 的元素被映射到输出 2D 张量中的索引 [j, i]。如果输入是一个方阵,tf.transpose() 的输入和输出形状将相同,但这不是一个逐元素操作,因为输出张量中 [i, j] 处的值不仅取决于输入张量中 [i, j] 处的值,而是取决于其他索引处的值。

B.2.2. 二元操作

与一元操作不同,二元操作需要两个输入参数。tf.add() 可能是最常用的二元操作。它可能也是最简单的,因为它只是简单地将两个张量相加。例如,

> const x = tf.tensor2d([[0, 2], [4, 6]]);
> const y = tf.tensor2d([[10, 20], [30, 46]]);
> tf.add(x, y).print();
Tensor
    [[10, 22],
     [34, 52]]

类似的二元操作包括

  • tf.sub() 用于两个张量的减法

  • tf.mul() 用于两个张量的乘法

  • tf.matMul() 用于计算两个张量之间的矩阵积

  • tf.logicalAnd()tf.logicalOr()tf.logicalXor() 用于在布尔类型张量上执行 AND、OR 和 XOR 操作。

一些二元操作支持 广播,或者对不同形状的两个输入张量进行操作,并根据某种规则将较小形状的输入元素应用于另一个输入的多个元素。详细讨论请参阅信息框 2.4 中的第二章。

B.2.3. 张量的拼接和切片

一元和二元操作是张量输入张量输出(TITO)的,它们以一个或多个张量作为输入,并返回一个张量作为输出。 TensorFlow.js 中的一些常用操作不是 TITO,因为它们将张量与另一个非张量参数一起作为输入。tf.concat()可能是这类函数中最常用的函数。它允许你将多个形状兼容的张量连接成一个单一的张量。只有当张量的形状满足某些约束条件时才能进行连接。例如,可以将[5, 3]张量和[4, 3]张量沿第一个轴合并以得到[9, 3]张量,但如果它们的形状分别为[5, 3][4, 2],则无法将它们组合在一起!在给定的形状合法性的情况下,你可以使用tf.concat()函数来连接张量。例如,以下代码沿第一个轴连接一个全零[2, 2]张量和一个全一[2, 2]张量,则得到一个[4, 2]张量,其中“顶部”是全零,而“底部”则是全一:

> const x = tf.zeros([2, 2]);
> const y = tf.ones([2, 2]);
> tf.concat([x, y]).print();
Tensor
    [[0, 0],
     [0, 0],
     [1, 1],
     [1, 1]]

由于两个输入张量的形状相同,可以以不同的方式对它们进行连接:即沿第二个轴进行连接。轴可以作为第二个输入参数传递给tf.concat()。这将给我们一个[2, 4]张量,在这个张量中,左半部分都是零,右半部分都是一:

> tf.concat([x, y], 1).print();
Tensor
    [[0, 0, 1, 1],
     [0, 0, 1, 1]]

除了将多个张量连接成一个以外,有时我们希望执行“反向”操作,检索张量的一部分。例如,假设你创建了一个形状为[3, 2]的二维张量(矩阵),

> const x = tf.randomNormal([3, 2]);
> x.print();
Tensor
    [[1.2366893 , 0.6011682 ],
     [-1.0172369, -0.5025602],
     [-0.6265425, -0.0009868]]

而你想要获取矩阵的第二行。为此,可以使用tf.slice()的链式版本:

> x.slice([1, 0], [1, 2]).print();
Tensor
     [[-1.0172369, -0.5025602],]

slice()的第一个参数指示我们想要的输入张量部分从第一个维度的索引 1 和第二个维度的索引 0 开始。换句话说,它应该从第二行和第一列开始,因为我们在这里处理的二维张量是一个矩阵。第二个参数指定所需输出的形状:[1, 2]或在矩阵语言中,1 行 2 列。

如您所看到的,通过查看打印的值,我们成功地检索了 3×2 矩阵的第二行。输出的形状与输入的秩相同(2),但第一个维度的大小为 1。在这种情况下,我们检索第二个维度的全部(所有列)和第一个维度的子集(行的一部分)。这是一种特殊情况,可以使用更简单的语法实现相同的效果:

> x.slice(1, 1).print();
Tensor
     [[-1.0172369, -0.5025602],]

在这个更简单的语法中,我们只需要指定请求的块沿第一个维度的起始索引和大小。如果将第二个输入参数传递为 1 而不是 2,则输出将包含矩阵的第一行和第二行:

> x.slice(1, 2).print();
Tensor
    [[-1.0172369, -0.5025602],
     [-0.6265425, -0.0009868]]

正如你可能猜到的那样,这种更简单的语法与批处理约定有关。它使得从批处理张量中获取单个示例的数据更容易。

但是如果我们想要访问矩阵的 呢?在这种情况下,我们将不得不使用更复杂的语法。例如,假设我们想要矩阵的第二列。可以通过以下方式实现

> x.slice([0, 1], [-1, 1]).print();
Tensor
    [[0.6011682 ],
     [-0.5025602],
     [-0.0009868]]

这里,第一个参数([0, 1])是表示我们想要的切片的起始索引的数组。它是沿第一维的第一个索引和第二维的第二个索引。更简单地说,我们希望我们的切片从第一行和第二列开始。第二个参数([-1, 1])指定了我们想要的切片的大小。第一个数字(-1)表示我们想要沿第一维的所有索引(我们想要所有起始行),而第二个数字(1)表示我们只想要沿第二维的一个索引(我们只想要一列)。结果是矩阵的第二列。

看一下 slice() 的语法,你可能已经意识到 slice() 不仅限于检索行或列。事实上,如果开始索引和大小数组被正确指定,它足够灵活,可以让你检索输入二维张量中的任何“子矩阵”(矩阵内的任何连续矩形区域)。更一般地,对于秩大于 0 的张量,slice() 允许你检索输入张量中的任何连续子张量。我们将这留作附录末尾的练习。

除了 tf.slice()tf.concat(),另外两个经常用于将张量分割成部分或将多个张量合并成一个的操作是 tf.unstack()tf.stack()tf.unstack() 将张量沿着第一维分割成多个“pieces”。每个片段在第一维上的尺寸为 1。例如,我们可以使用 tf.unstack() 的链式 API:

> const x = tf.tensor2d([[1, 2], [3, 4], [5, 6]]);
> x.print();
Tensor
    [[1, 2],
     [3, 4],
     [5, 6]]
> const pieces = x.unstack();
> console.log(pieces.length);
  3
> pieces[0].print();
Tensor
    [1, 2]
> pieces[1].print();
Tensor
    [3, 4]
> pieces[2].print();
Tensor
    [5, 6]

你可能已经注意到,unstack() 返回的“pieces”比输入张量的秩少一。

tf.stack()tf.unstack() 的反向操作。顾名思义,它将具有相同形状的多个张量“堆叠”到一个新张量中。根据先前的示例代码片段,我们将片段重新堆叠在一起:

> tf.stack(pieces).print();
Tensor
    [[1, 2],
     [3, 4],
     [5, 6]]

tf.unstack() 用于从批处理张量中获取与各个示例对应的数据;tf.stack() 用于将各个示例的数据合并成一个批处理张量。

B.3. TensorFlow.js 中的内存管理:tf.dispose()tf.tidy()

在 TensorFlow.js 中,如果你直接处理张量对象,你需要对它们执行内存管理。特别是在创建和使用张量后,张量需要被释放,否则它将继续占用分配给它的内存。如果未释放的张量数量过多或者总大小过大,它们最终将导致浏览器标签页耗尽 WebGL 内存或导致 Node.js 进程耗尽系统或 GPU 内存(取决于是否使用 tfjs-node 的 CPU 或 GPU 版本)。TensorFlow.js 不会自动对用户创建的张量进行垃圾回收。[5] 这是因为 JavaScript 不支持对象终结。TensorFlow.js 提供了两个内存管理函数:tf.dispose()tf.tidy()

然而,在 TensorFlow.js 函数和对象方法中创建的张量由库本身管理,因此你不需要担心在调用这些函数或方法时包装它们在tf.tidy()中。其中的示例函数包括tf.confusionMatrix()tf.Model.predict()tf.Model.fit()

例如,考虑使用for循环对 TensorFlow.js 模型进行重复推理的示例:

const model = await tf.loadLayersModel(                                    ***1***
    'https://storage.googleapis.com/tfjs-models/tfjs/iris_v1/model.json'); ***1***
const x = tf.randomUniform([1, 4]);                                        ***2***
for (let i = 0; i < 3; ++i) {
  const y = model.predict(x);
  y.print();
  console.log(`# of tensors: ${tf.memory().numTensors}` );                 ***3***
}
  • 1 从网络上加载预先训练好的模型

  • 2 创建一个虚拟输入张量

  • 3 检查当前已分配张量的数量

输出将如下所示

Tensor
     [[0.4286409, 0.4692867, 0.1020722],]
# of tensors: 14
Tensor
     [[0.4286409, 0.4692867, 0.1020722],]
# of tensors: 15
Tensor
     [[0.4286409, 0.4692867, 0.1020722],]
# of tensors: 16

正如你在控制台日志中看到的那样,每次调用model.predict()都会生成一个额外的张量,在迭代结束后不会被释放。如果允许for循环运行足够数量的迭代,它最终会导致内存不足错误。这是因为输出张量y没有被正确释放,导致张量内存泄漏。有两种方法可以修复这个内存泄漏。

在第一种方法中,你可以在不再需要输出张量时调用tf.dispose()

for (let i = 0; i < 3; ++i) {
  const y = model.predict(x);
  y.print();
  tf.dispose(y);                                          ***1***
  console.log(`# of tensors: ${tf.memory().numTensors}` );
}
  • 1 在使用后释放输出张量

在第二种方法中,你可以在for循环的主体部分使用tf.tidy()

for (let i = 0; i < 3; ++i) {
  tf.tidy(() => {                          ***1***
    const y = model.predict(x);
    y.print();
    console.log(`# of tensors: ${tf.memory().numTensors}` );
  });
}
  • 1 tf.tidy()自动释放传递给它的函数中创建的所有张量,除了由该函数返回的张量。

无论选用哪种方法,你应该看到迭代中分配的张量数量变为常数,表明不再有张量内存泄漏。哪种方法应该优先使用呢?通常,你应该使用tf.tidy()(第二种方法),因为它消除了跟踪需要释放哪些张量的需要。tf.tidy()是一个智能函数,它释放传递给它的匿名函数中创建的所有张量(除了由该函数返回的张量-稍后再说),即使这些张量没有绑定到任何 JavaScript 对象。例如,假设我们稍微修改先前的推理代码,以便使用argMax()获得获胜类别的索引:

const model = await tf.loadLayersModel(
    'https://storage.googleapis.com/tfjs-models/tfjs/iris_v1/model.json');
const x = tf.randomUniform([1, 4]);
for (let i = 0; i < 3; ++i) {
  const winningIndex =
           model.predict(x).argMax().dataSync()[0];
  console.log(`winning index: ${winningIndex}`);
  console.log(`# of tensors: ${tf.memory().numTensors}` );
}

当这段代码运行时,你会发现它每次迭代泄漏两个张量:

winning index: 0
# of tensors: 15
winning index: 0
# of tensors: 17
winning index: 0
# of tensors: 19

为什么每次迭代泄漏两个张量?因为这行代码:

   const winningIndex =
       model.predict(x).argMax().dataSync()[0];

生成两个新的张量。第一个是model.predict()的输出,第二个是argMax()的返回值。这两个张量都没有绑定到任何 JavaScript 对象上。它们被创建后立即使用。这两个张量在某种意义上“丢失”——没有 JavaScript 对象可供您用来引用它们。因此,tf.dispose()不能用于清理这两个张量。但是,tf.tidy()仍然可以用来修复内存泄漏,因为它会对新张量执行簿记,无论它们是否绑定到 JavaScript 对象上:

const model = await tf.loadLayersModel(
    'https://storage.googleapis.com/tfjs-models/tfjs/iris_v1/model.json');
const x = tf.randomUniform([1, 4]);
for (let i = 0; i < 3; ++i) {
  tf.tidy(() => {                                                  ***1***
    const winningIndex = model.predict(x).argMax().dataSync()[0];
    console.log(`winning index: ${winningIndex}`);
    console.log(`# of tensors: ${tf.memory().numTensors}` );       ***1***
  });
}
  • 1 tf.tidy()会自动处理传递给它作为参数的匿名函数中创建的张量,即使这些张量没有绑定到 JavaScript 对象上。

tf.tidy()的示例用法操作的是不返回任何张量的函数。如果该函数返回张量,则不希望将它们处理掉,因为它们需要在后续使用。这种情况在使用 TensorFlow.js 提供的基本张量操作编写自定义张量操作时经常遇到。例如,假设我们想编写一个函数来计算输入张量的标准化值——即,减去平均值并将标准差缩放为 1 的张量:

function normalize(x) {
  const mean = x.mean();
  const sd = x.norm(2);
  return x.sub(mean).div(sd);
}

这个实现有什么问题?^([6]) 就内存管理而言,它泄漏了三个张量:1)均值,2)SD 和 3)一个更微妙的泄漏:sub()调用的返回值。为了修复内存泄漏问题,我们将函数体包装在tf.tidy()中:

这个实现还有其他问题。例如,它没有对输入张量进行健全性检查,以确保它至少有两个元素,使 SD 不为零,否则将导致除以零和无限结果。但是这些问题与此处的讨论无直接关联。

function normalize(x) {
  return tf.tidy(() => {
    const mean = x.mean();
    const sd = x.norm(2);
    return x.sub(mean).div(sd);
       });
}

在这里,tf.tidy()为我们完成了三个操作:

  • 它会自动处理在匿名函数中创建但未被它返回的张量,包括之前提到的所有泄漏。我们在之前的例子中已经看到这一点了。

  • 它检测到div()调用的输出由匿名函数返回,因此将其转发到自己的返回值。

  • 在此期间,它将避免处理该特定张量,以便它可以在tf.tidy()调用之外使用。

如我们所见,tf.tidy() 是一种智能而强大的内存管理函数。它在 TensorFlow.js 代码库中被广泛使用。在本书的示例中,您还将经常看到它。然而,它有以下重要限制:作为参数传递给 tf.tidy() 的匿名函数 能是异步的。如果您有一些需要内存管理的异步代码,您应该使用 tf.dispose() 并手动跟踪待处理的张量。在这种情况下,您可以使用 tf.memory().numTensor 来检查泄漏张量的数量。一个好的做法是编写单元测试来断言不存在内存泄漏。

B.4. 计算梯度

这一部分适用于对在 TensorFlow.js 中执行导数和梯度计算感兴趣的读者。对于本书中的大多数深度学习模型,导数和梯度的计算都是在model.fit()model.fitDataset()中自动完成的。然而,对于某些问题类型,比如在第七章中找到卷积滤波器的最大激活图像和在第十一章中的 RL,需要显式地计算导数和梯度。TensorFlow.js 提供了支持此类用例的 API。让我们从最简单的情景开始,即一个接受单个输入张量并返回单个输出张量的函数:

const f = x => tf.atan(x);

为了计算函数(f)相对于输入(x)的导数,我们使用tf.grad()函数:

const df = tf.grad(f);

注意,tf.grad()不会立即给出导数的值。相反,它会给出一个函数,即原始函数(f)的导数。您可以使用具体的x值调用该函数(df),这时您就会得到df/dx的值。例如,

const x = tf.tensor([-4, -2, 0, 2, 4]);
df(x).print();

它给出了一个输出,正确反映了在 x 值为-4、-2、0、2 和 4 时atan()函数的导数(参见图 B.5):

Tensor
    [0.0588235, 0.2, 1, 0.2, 0.0588235]
图 B.5. 函数atan(x)的图表

tf.grad()仅适用于具有单个输入张量的函数。如果您有一个具有多个输入的函数怎么办?让我们考虑一个简单的例子h(x, y),它只是两个张量的乘积:

const h = (x, y) => x.mul(y);

tf.grads()(带有名称中的“s”)生成一个函数,该函数返回输入函数相对于所有参数的偏导数:

const dh = tf.grads(h);
const dhValues = dh([tf.tensor1d([1, 2]), tf.tensor1d([-1, -2])]);
dhValues[0].print();
dhValues[1].print();

这给出了结果

Tensor
    [-1, -2]
Tensor
    [1, 2]

这些结果是正确的,因为相对于x的偏导数*yy,相对于y的偏导数是x

tf.grad()tf.grads()生成的函数只给出导数,而不是原始函数的返回值。在h(x, y)的示例中,如果我们不仅想要得到导数,还想要h的值,那么可以使用tf.valueAndGrads()函数:

const vdh = tf.valueAndGrads(h);
const out = vdh([tf.tensor1d([1, 2]), tf.tensor1d([-1, -2])]);

输出(out)是一个具有两个字段的对象:value,即给定输入值时h的值,以及grads,其格式与由tf.grads()生成的函数的返回值相同,即偏导数张量的数组:

out.value.print();
out.grads[0].print();
out.grads[1].print();

Tensor
    [-1, -4]
Tensor
    [-1, -2]
Tensor
    [1, 2]

讨论的 API 都涉及到计算函数相对于其显式参数的导数。然而,在深度学习中的一个常见情景是函数在计算中使用了权重。这些权重表示为 tf.Variable 对象,并且作为参数明确传递给函数。对于这样的函数,我们经常需要在训练过程中计算相对于权重的导数。tf.variableGrads() 函数就是为此工作流程提供支持的,它跟踪被求导函数所访问的可训练变量,并自动计算相对于它们的导数。考虑以下示例:

const trainable = true;
const a = tf.variable(tf.tensor1d([3, 4]), trainable, 'a');
const b = tf.variable(tf.tensor1d([5, 6]), trainable, 'b');
const x = tf.tensor1d([1, 2]);

const f = () => a.mul(x.square()).add(b.mul(x)).sum();     ***1***
const {value, grads} = tf.variableGrads(f);
  • 1 f(a, b) = a * x ^ 2 + b * x。调用 sum() 方法是因为 tf.variableGrads() 要求被求导函数返回一个标量。

tf.variableGrads() 的输出中的 value 字段是在给定 abx 的当前值时 f 的返回值。 grads 字段是一个 JavaScript 对象,它在相应的键名下携带了对两个变量(ab)的导数。例如,f(a, b) 关于 a 的导数是 x ^ 2f(a, b) 关于 b 的导数是 x

grads.a.print();
grads.b.print();

这正确给出了

Tensor
    [1, 4]
Tensor
    [1, 2]

练习

  1. 使用tf.tensorBuffer()创建符合以下条件的“恒等 4D 张量”。它的形状应为[5, 5, 5, 5],它应该在所有位置都有 0 值,除了元素索引为四个相同数字(例如,[2, 2, 2, 2])的元素应具有值 1。

  2. 使用tf.randomUniform()和默认的[0,1)间隔创建一个形状为[2,4,5]的 3D 张量。使用tf.sum(),编写一行代码对第二和第三维进行减少求和。检查输出。它应该具有形状[2]。你预计元素的值是多少,大约是多少?输出是否符合你的期望?(提示:在[0,1)间隔中随机分布的数字的期望值是多少?考虑到统计独立性,这两个值的总和的期望值是多少?)

  3. 使用tf.randomUniform()创建一个 4x4 矩阵(形状为[4,4]的 2D 张量)。使用tf.slice()获取位于中心的 2x2 子矩阵。

  4. 使用tf.ones()tf.mul()tf.concat()创建这样一个 3D 张量:其形状应为[5,4,3]。沿第一轴的第一片(形状为[1,4,3]的张量)应具有所有元素值为 1;第二片沿第一轴应具有所有元素值为 2; 依此类推。

    1. 加分题:该张量具有许多元素,因此仅通过查看print()的文本输出很难测试其正确性。你如何编写单元测试来检查其正确性?(提示:使用data()dataSync()arraySync())。
  5. 编写一个 JavaScript 函数,它对两个相同形状的输入 2D 张量(矩阵)执行以下操作。首先,将这两个矩阵相加。其次,逐个元素地将结果矩阵除以 2。第三,矩阵被转置。返回转置操作的结果。

    1. 你使用了哪些 TensorFlow.js 函数来编写这个函数?

    2. 你能实现这个函数两次,一次使用功能 API,一次使用链接 API 吗?哪个实现看起来更清晰、更易读?

    3. 哪些步骤涉及广播?

    4. 你如何确保这个函数不会泄漏内存?

    5. 你能写一个单元测试(使用 Jasmine 库,位于 jasmine.github.io/)来确保不存在内存泄漏吗?

术语表

激活函数

神经网络层的最后阶段的函数。例如,可以在矩阵乘法的结果上应用修正线性单元(relu)函数,以生成密集层的最终输出。激活函数可以是线性或非线性的。非线性激活函数可用于增加神经网络的表示能力(或容量)。非线性激活函数的示例包括 sigmoid、双曲正切(tanh)和前面提到的 relu。

曲线下面积(AUC)

用于量化 ROC 曲线形状的单个数字。它被定义为 ROC 曲线下的定积分,从假阳性率 0 到 1。见 ROC 曲线

在 TensorFlow.js 的上下文中,当我们谈论一个 张量 时,一个轴(复数 )是张量中独立索引的一个关键。例如,一个秩为 3 的张量有三个轴;秩为 3 的张量的一个元素由三个整数标识,这些整数对应于三个轴。也称为 维度

反向传播

将可微分机器学习模型的损失值追溯到权重参数梯度的算法。它基于微分的链式法则,并构成了本书中大多数神经网络的训练基础。

时间反向传播(BPTT)

一种特殊形式的反向传播,在其中步骤不是在模型的连续层的操作上进行,而是在连续时间步骤上进行的。它构成了递归神经网络(RNNs)的训练基础。

平衡(数据集)

具有分类标签的数据集的一种质量。不同类别的示例数量越平衡,数据集就越平衡。

批次

在神经网络的训练过程中,通常将多个输入示例聚合成一个张量,该张量用于计算梯度和对网络权重的更新。这样的聚合称为 批次。批次中示例的数量称为 批次大小

贝尔曼方程

在强化学习中,一种递归方程,将状态-动作对的价值量化为两项之和:1)代理在采取行动后立即获得的奖励;2)代理在下一个状态中可以获得的最佳期望奖励,乘以一个折现因子。第二项假定在下一个状态中选择的行动是最优的。它构成了强化学习算法(如深度 Q 学习)的基础。

二元分类

一个分类任务,其中目标是回答是/否问题,例如某个 X 射线图像是否表明肺炎,或者信用卡交易是否合法或欺诈性。

广播

TensorFlow 允许对具有不同但兼容形状的张量进行成对操作。例如,可以将形状为 [5] 的张量添加到形状为 [13, 5] 的张量中。实际上,较小的张量将重复 13 次以计算输出。关于何时允许广播的规则的详细信息在 章节 2.4 的信息框 中有说明。

容量

机器学习模型能够学习的输入-输出关系的范围。例如,具有具有非线性激活函数的隐藏层的神经网络比线性回归模型具有更大的容量。

类激活图

一种算法,可以可视化输入图像的不同部分对卷积神经网络分类输出的相对重要性。它基于计算网络的最后一个内部卷积层的输出对获胜类别的最终概率得分的梯度。详细讨论见 第 7.2.3 节。

计算机视觉

计算机如何理解图像和视频的研究。这是机器学习的重要组成部分。在机器学习的背景下,常见的计算机视觉任务包括图像识别、分割、字幕生成和目标检测。

混淆矩阵

一个形状为 [numClasses, numClasses] 的方阵(2D 张量)。在多类分类中,混淆矩阵用于量化给定真实类别的示例被分类为每个可能类别的次数。索引 [i, j] 处的元素是真实类别 i 的示例被分类为类别 j 的次数。对角线上的元素对应于正确分类的结果。

常量折叠

一种计算图优化类型,其中包含仅由预定常量节点和它们之间的确定性操作组成的子图被减少为单个常量节点。TensorFlow.js 中的 GraphModel 转换技术利用了常量折叠。

卷积核

在卷积操作中,一个张量对输入张量进行操作以生成输出张量。以图像张量为例:与输入图像相比,卷积核通常在其高度和宽度维度上较小。它在输入图像的高度和宽度维度上“滑动”,并在每个滑动位置上进行点积(乘法和加法)。对于 TensorFlow.js 的卷积层(例如 conv2d),卷积核是其关键权重。

数据增强

通过创建训练样本(x,y)的变异来从现有训练样本中生成更多训练数据的过程,通过产生有效输入 x'的一系列编程转换来暴露模型更多数据的方面,从而更好地泛化,而不需要工程师手动将这些转换类型的不变性构建到模型中。

深度学习

深度神经网络的研究和应用(即使用大量连续的表示转换来解决机器学习问题)。

深度神经网络

具有大量层(从两个到数千个)的神经网络。

维度

在张量的上下文中,与同义。 参见

点积

参见内积

嵌入

在深度学习中,将某个数据片段表示为n维向量空间(n为正整数)的表示。 换句话说,它是将数据片段表示为有序的,长度为n的浮点数数组。 可以为许多类型的数据创建嵌入表示:图像,声音,单词以及来自封闭集的项目。 嵌入通常来自训练的神经网络的中间层。

集成学习

训练一些个体机器学习模型并在同一问题上一起使用它们进行推断的实践。 即使每个单独的模型可能不太准确,但集成模型的准确性可能会更高。 集成模型经常被数据科学竞赛的获奖作品使用,例如 Kaggle 竞赛。

纪元

在训练模型时,对训练数据的完整通过。

Epsilon-贪婪策略

在强化学习中,一种参数化代理方的随机探索行为与最优行为之间平衡的动作选择方法。 epsilon 的值受到 0 和 1 之间的约束。 它越高,代理选择随机动作的可能性就越大。

示例

在机器学习的上下文中,输入数据的个体实例(例如,适用于计算机视觉模型的图像),机器学习模型将为其生成输出预测(例如,图像的标签)。

特征

机器学习模型的输入数据的一个方面。 特征可以采用以下任何形式:

  • 数字(例如信用卡交易的货币金额)

  • 来自开放集合的字符串(交易名称)

  • 分类信息的一部分(例如信用卡品牌名称)

  • 一个或多维数组(例如,信用卡客户签名的灰度图像表示为 2D 数组)

  • 其他类型的信息(例如日期时间)

输入示例可以由一个或多个特征组成。

特征工程

原始特征数据的转化过程,使其更易于解决机器学习问题。在深度学习之前,通过领域专家进行试错的特征工程。这通常是一个耗时且脆弱的过程,没有找到最优解的保证。深度学习在很大程度上自动化了特征工程。

微调

在迁移学习中,模型训练的一个阶段,在该阶段,基本模型中某些层的权重可以更新。通常,在全部基本模型的权重都被冻结以防止大的初始梯度干扰预训练权重的初始阶段之后。如果使用得当,微调可以增强迁移学习模型的能力,从而在消耗比完全从头开始训练模型少得多的计算资源的同时实现更高的准确性。

生成式对抗网络(GAN)

生成式机器学习模型的一种类型,包括两个部分:鉴别器和生成器。鉴别器被训练为区分真实样本和训练集中的假样本,而生成器被训练为输出能使鉴别器给出高真实度评分的样本(即“欺骗”鉴别器,“使其认为”这些假样本是真实的)。经过适当的训练,生成器能够输出高度逼真的假样本。

金值

在测试机器学习系统时,模型对于给定输入应该生成的正确输出。例如,当给出贝多芬第五交响曲的录音时,神经网络将其分类为音乐流派时的“标准标签”就是一个例子。

梯度下降

梯度下降法是通过沿着系统参数的梯度方向(即与输出值相关的参数的导数)反复改变系统参数,将系统数值输出最小化的过程。这是神经网络训练的主要方法。在神经网络训练的上下文中,系统由工程师选择的神经网络和损失函数组成。系统的参数是神经网络层的权重,迭代过程逐批次地在训练数据上进行。

图形处理单元(GPU)

配备比典型 CPU 核心多得多(数百或数千个)的并行计算芯片。 GPU 最初被设计用于加速 2D 和 3D 图形的计算和渲染。但它们也被证明对于运行深度神经网络所涉及的并行计算非常有用。GPU 是深度学习革命的重要因素,并且在今天深度学习的研究和应用中继续发挥关键作用。TensorFlow.js 通过两个渠道利用 GPU 的并行计算能力:1)web 浏览器的 WebGL API,2)在 Node.js 中绑定到 TensorFlow CUDA 核心。

GraphModel

在 TensorFlow.js 中,从 TensorFlow(Python)转换并加载到 JavaScript 中的模型。 GraphModel 有潜力进行 TensorFlow 内部性能优化,比如 Grappler 的算术优化和 op 融合(详见第 12.2.2 节)。

隐藏层

由一个输出不作为网络的输出暴露,而是只被网络的其他层消耗的层组成的神经网络。例如,在一个定义为 TensorFlow.js 顺序模型的神经网络中,除了最后一个层外,所有层都是隐藏层。

超参数优化

有时也称为超参数调优;搜索在给定机器学习任务上给出最低验证损失的超参数集的过程。

超参数

模型和优化器的可调参数,这些参数不能通过反向传播进行调整。通常学习率和模型结构是常见的超参数示例。超参数可以通过网格搜索或更复杂的超参数调优算法进行调整。

假设空间

在机器学习的上下文中,机器学习问题的可能解集。训练过程涉及在这样的空间中搜索一个良好的解。假设空间由解决问题选择的机器学习模型的类型和架构决定。

ImageNet

由标记的彩色图像组成的大规模公共数据集。这是计算机视觉导向的深度神经网络的重要训练集和基准。ImageNet 在引领深度学习革命的开端方面发挥了重要作用。

填补

一种从数据集中填补缺失值的技术。例如,如果我们有一个汽车数据集,有些汽车缺少“重量”特征,我们可以简单地猜测这些特征的平均重量。也可以使用更复杂的填补技术。

形式

一种有大量层和复杂网络结构的深度卷积神经网络。

独立同分布(IID)

数据样本的统计属性。如果我们假设数据是从一个潜在的分布中抽样得到的,那么如果每个样本来自相同的分布,则样本是相同分布的。如果知道一个样本的值不会给你关于下一个样本的额外信息,那么样本是独立的。

掷骰子的样本是 IID(独立同分布)样本集的一个示例。如果骰子的结果被排序,那么样本是相同分布的但不是独立的。训练数据应该是 IID,否则在训练过程中可能会出现收敛或其他问题。

推断

对输入数据使用机器学习模型生成输出。这是训练模型的最终目的。

内积

也称为点积;是两个形状相同的向量上的数学运算,得到一个单一的标量值。要计算向量ab之间的内积,对所有有效值的i求和a[i] * b[i]。从几何角度来看,两个向量的内积等于它们的大小乘积和它们之间的余弦值。

Keras

一种流行的深度学习库。今天,在 Kaggle 竞赛中它是使用最频繁的深度学习库。弗朗索瓦·肖莱(François Chollet)是其原始作者,目前是 Google 的软件工程师。Keras 是一个 Python 库。TensorFlow.js 的高级 API,这是本书的重点,是基于 Keras 建模并与之兼容的。

标签

根据手头的任务给定输入示例的期望答案。标签可以是布尔(是/否)答案、数字、文本字符串、一系列可能的类别中的一个、一系列数字或更复杂的数据类型。在监督式机器学习中,模型的目标是生成与标签尽可能匹配的输出。

在神经网络的上下文中,数据表示的转换。它的行为类似于数学函数:给定一个输入,它产生一个输出。一个层可以有由它的权重捕获的状态。这些权重可以在神经网络的训练过程中被改变。

LayersModel

使用 TensorFlow.js 的类似 Keras 的高级 API 构建的模型。它也可以从转换后的 Keras(Python)模型加载。LayersModel支持推断(使用其predict()方法)和训练(使用其fit()fitDataset()方法)。

学习率

在梯度下降期间,模型权重被修改以减少损失。权重的确切变化不仅取决于损失的梯度,还取决于一个参数。在标准梯度下降算法中,通过将梯度乘以学习率来计算权重更新,学习率通常是一个小正常数。tensorflow.js 中 'sgd' 优化器的默认学习率是 0.01。

局部最小值

在优化模型参数时,参数的设置使得参数的任何足够小的变化都会增加损失。类似于碗底的大理石,没有任何更低的小运动。局部最小值与全局最小值有所区别,局部最小值是局部邻域中的最低点,而全局最小值是整体上的最低点。

对数几率

在机器学习中,一个未标准化的概率值。与概率不同,对数几率不限于[0,1]区间,也不要求总和为 1。因此,它们可以更轻松地由神经网络层输出。一组对数几率可以通过称为softmax的操作标准化为概率值。

机器学习

一种人工智能(AI)的子领域,通过使用带有所需答案标签的数据自动发现解决复杂问题的规则。它与经典编程不同,因为它不涉及规则的手工制作。

马尔可夫决策过程(MDP)

在强化学习中,一种决策过程,其中智能体选择的当前状态和动作完全决定了智能体将结束的下一个状态以及智能体将在该步骤中获得的奖励。这是一个重要的简化,使得像 Q-learning 这样的学习算法成为可能。

模型

在机器学习和深度学习中,将输入数据(例如图像)转换为所需输出(例如图像的文本标签)的对象,通过一系列连续的数学操作。模型具有可以在训练期间调整的参数(称为权重)。

模型适应

训练预训练模型或其部分的过程,以使模型在特定用户或特定用例的输入数据上进行推理时达到更好的准确性。这是一种迁移学习的类型,其中输入特征的类型和目标的类型与原始模型不同。

模型部署

将训练好的模型打包到可以用于进行预测的地方的过程。类似于其他软件堆栈的“推向生产”,部署是用户可以真正使用模型的方式。

MobileNet

一个预训练的深度卷积神经网络。通常在 ImageNet 图像分类数据集上进行训练,并可用于迁移学习。在类似的预训练卷积神经网络中,它具有相对较小的尺寸,并且在执行推理时涉及的计算较少,因此更适合在资源受限的环境(如 Web 浏览器)中运行,使用 TensorFlow.js。

多类分类

一个分类问题,其中目标可能具有两个以上的离散标签。例如,一张图片包含什么样的动物或给定内容的网页所使用的(自然)语言是什么。

多热编码

一种通过将与单词对应的元素设置为 1 并将其余元素保持为 0 来表示句子中的单词(或一般情况下,序列中的项目)的方法。这可以看作是独热编码的泛化。它丢弃了关于单词顺序的信息。

多层感知器(MLP)

由前馈拓扑和至少一个隐藏层组成的神经网络。

自然语言处理

计算机科学的一个子领域,研究如何利用计算机来处理和理解自然语言,最突出的是文本和语音。深度学习在自然语言处理中有许多应用。

神经网络

一类受生物神经系统中分层组织启发的机器学习模型。神经网络的层对数据表示进行多步、可分离的转换。

非线性

一个输入输出关系,不符合线性定义(输入的线性组合导致输出的线性组合,最多存在一个常数项的差异)。在神经网络中,非线性关系(例如在层中的 sigmoid 和 relu 激活)以及多个这样的关系的级联可以增加神经网络的容量。

目标检测

一种计算机视觉任务,涉及在图像中检测某些类别的对象及其位置。

独热编码

将分类数据编码为长度为N的向量的方案,该向量由除了对应于实际类别的索引之外的所有零组成。

操作融合

一种计算图优化技术,其中多个操作(或 ops)被替换为一个等效的操作。操作融合减少了操作分派开销,并可以为进一步的操作内存和性能优化提供更多机会。

超出词汇表(OOV)

在深度学习的上下文中,当一个词汇表用于一组离散项时,该词汇表有时不包括所有可能的项。当遇到词汇表之外的项时,它被映射到一个称为超出词汇表的特殊索引,然后可以将其映射到独热编码或嵌入表示中的特殊元素。见词汇表

过拟合

当模型被适配到训练数据中,以至于模型具有足够的容量来记忆训练数据时,我们会看到训练损失继续下降,但测试或验证损失开始上升。具有这种属性的模型开始失去泛化能力,仅在训练数据中的确切样本上表现良好。我们称处于这种情况下的模型为过拟合。

策略梯度

一种强化学习算法,它计算并利用选定动作的某些度量(如对数几率)相对于策略网络的权重的梯度,以使策略网络逐渐选择更好的动作。

精确度

一个二元分类器的度量,定义为分类器标记为正且实际为正的比率。参见 召回率

伪示例

基于已知有效的输入训练示例的突变的附加示例,用于补充训练数据。例如,我们可以取 MNIST 数字并应用小的旋转和倾斜。这些变换不会改变图像标签。

Q 网络

在强化学习中,预测给定当前状态观察到所有可能行动的 Q 值的神经网络。Q 学习算法是关于使用代理经验数据训练 Q 网络的过程。

Q 值

在强化学习中,以给定状态下采取行动的预期总未来累积奖励。因此,一个 Q 值是一个行动和状态的函数。它指导了在 Q 学习中选择行动的过程。

随机初始化

在模型拟合之前,为权重分配初始值作为起点的过程。关于什么,具体来说,是好的分布选择以获取初始值的文献基于层类型、大小和任务有很多。

回想

一个二元分类器的度量,定义为分类器标记为正的实际示例的比率。参见 精度

回归

一种学习问题,期望输出(或标签)是一个数字或数字列表。预测越接近期望输出,越好。

正则化

在机器学习中,对损失函数或训练过程进行各种修改以抵消过度拟合的过程。有几种方式可以执行正则化,其中最常用的是 L1 和 L2 正则化。

强化学习(RL)

一种涉及通过与环境交互来学习最优决策以最大化一个叫做奖励的度量的机器学习方法。本书的第十一章介绍了 RL 的基础知识以及如何使用深度学习技术解决简单的 RL 问题。

ResNet

Residual Network 的缩写;一种广泛用于计算机视觉的流行卷积网络,具有残差连接,即跳过层之间的连接。

ROC 曲线

可视化二元分类器真正的阳性率(召回率)和假阳性率(误报率)之间的权衡的方法。该曲线的名称(接受者操作特征曲线)源于雷达技术的早期阶段。参见 曲线下面积(AUC)。

频谱图

一种类似于图像的二维表示,用于表示一维时间信号(例如声音)。频谱图有两个维度:时间和频率。每个元素表示在给定时间内在给定频率范围内所包含声音的强度或功率。

监督学习

使用标记示例训练机器学习模型的范例。模型的内部参数被改变,以最小化模型对示例的输出与相应实际标签之间的差异。

符号张量

在 TensorFlow.js 中,SymbolicTensor 类的对象,它是张量的形状和数据类型(dtype)的规范。与张量不同,SymbolicTensor 对象没有与具体值相关联。相反,它被用作层或模型的输入或输出的占位符。

张量

一种用于保存数据元素的数据结构,通常是数字。张量可以被视为 n 维网格,其中网格中的每个位置恰好保存一个元素。张量的维数和每个维度的大小被称为张量的 形状。例如,一个 3 × 4 矩阵是一个形状为 [3, 4] 的张量。长度为 10 的向量是一个形状为 [10] 的一维张量。每个张量实例只保存一种类型的元素。张量是这样设计的,因为它允许方便、高效的实现深度学习中常见操作:例如,矩阵点积。

TensorBoard

一个用于 TensorFlow 的监控和可视化工具。它允许用户在浏览器中可视化模型结构和训练性能。TensorFlow.js 可以将训练日志写入与 TensorBoard 兼容的数据格式。

TensorFlow

一个用于加速机器学习的开源 Python 库,重点放在深度神经网络上。它由 Google 的 Brain 团队于 2015 年 11 月发布。其 API 构成了 TensorFlow.js 的蓝图。

训练

改变机器学习模型的内部参数(权重),使模型的输出更接近期望的答案的过程。

训练数据

用于训练机器学习模型的数据。训练数据由各个示例组成。每个示例都是结构化信息(例如,图像、音频或文本),与预期答案(标签)一起。

迁移学习

将之前针对一个任务训练过的机器学习模型,重新训练它以适应一个新任务的相对较少的数据量(与原始训练数据集相比),并在新任务上进行推断的实践。

欠拟合

当一个模型经过太少的优化步骤训练,或者一个模型的表示能力(容量)不足以学习训练数据中的模式时,导致模型不能达到一个体面的质量水平时,我们称该模型为欠拟合。

无监督学习

使用未标记数据的机器学习范式。与使用标记数据的监督学习相对。无监督学习的示例包括聚类(在数据集中发现不同子集的示例)和异常检测(确定给定示例与训练集中的示例是否足够不同)。

验证数据

用于调整超参数的训练数据以外的数据,例如学习率或密集层中的单元数。验证数据允许我们调整学习算法,可能需要多次运行训练。由于验证数据与测试数据是分开的,我们仍然可以依靠测试数据的结果来提供无偏的模型在新的、未见过的数据上的性能估计。

梯度消失问题

在训练深度神经网络中的一个经典问题是,随着层数的增加,权重参数上的梯度越来越小,结果导致权重参数与损失函数之间的距离越来越远。在现代深度学习中,通过改进的激活函数、适当初始化权重和其他技巧来解决这个问题。

矢量化

将非数字数据转化为数字数组(如向量)的过程。例如,文本向量化涉及将字符、单词或句子转换为向量。

可视化工具

在与 TensorFlow.js 紧密集成的可视化库 tfjs-vis 中,可以通过在网页的一侧进行单个函数调用来创建一个可折叠的区域,以容纳可视化用的表面。可以在可视化工具中创建多个标签页来组织各个表面。详见第 8.1 节。

词汇表

在深度学习的背景下,一组离散的、独特的项目,可以用作神经网络的输入或输出。通常,词汇表的每个项目可以映射到一个整数索引,然后可以将其转换为一种 one-hot 或基于嵌入的表示。

权重

神经网络层的一个可调参数。改变权重会改变输入如何转换为输出的数值细节。神经网络的训练主要是关于以系统性的方式更新权重值。

权重量化

一种用于减少模型序列化和传输时大小的技术。它涉及将模型的权重参数以较低的数值精度存储。

词嵌入模型

文本相关的神经网络中将词向量化的一种方式。通过嵌入查找过程,将单词映射到 1D 张量(或向量)。与 one-hot 编码不同,词嵌入涉及到非稀疏向量,其中元素值是连续变化的数字,而不是 0 和 1。