Java-深度学习秘籍-全-

Java 深度学习秘籍(全)

原文:annas-archive.org/md5/f5d28e569048a2e5329b5ab42aa19b12

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

深度学习帮助了许多行业/公司解决重大挑战,提升了产品并增强了基础设施。深度学习的优势在于,你既无需设计决策算法,也不必决定数据集中的重要特征。你的神经网络能够完成这两项任务。我们已经看过足够多的理论书籍,这些书籍讲解了复杂的概念,却让读者感到困惑。了解你学到的知识如何/何时应用同样重要,特别是在企业相关的领域。这对于深度学习等先进技术来说尤为重要。你可能已经完成了毕业设计项目,但你还希望将自己的知识提升到一个新的层次。

当然,在企业开发中有一些最佳实践是我们在本书中可能没有涉及到的。如果在生产环境中部署过于繁琐,我们不希望读者质疑开发应用程序的目的。我们希望提供一种非常直接的方式,面向全球最大规模的开发者社区。正是基于这个原因,我们在全书中使用了DL4J(即Deeplearning4j)来演示示例。它提供了 DataVec 用于ETL(即提取、转换和加载),ND4J 作为科学计算库,以及 DL4J 核心库,用于开发和部署神经网络模型。DL4J 在某些情况下超越了市场上一些主要的深度学习库。我们并不是贬低其他库,因为这取决于你想用它们做什么。如果你不想频繁切换技术栈,也可以尝试在不同的阶段使用多个库。

本书适用人群

为了充分利用本书,我们建议读者具有深度学习和数据分析的基础知识。最好读者具备 MLP(多层感知机)或前馈网络、递归神经网络、LSTM、词向量表示以及一定的调试技能,以便能够理解错误栈中的错误。由于本书主要面向 Java 和 DL4J 库,读者应当具备扎实的 Java 和 DL4J 知识。本书不适合刚接触编程或没有深度学习基础的读者。

本书内容

第一章,Java 中的深度学习简介,简要介绍了如何使用 DL4J 进行深度学习。

第二章,数据提取、转换与加载,讨论了使用示例帮助处理神经网络数据的 ETL 过程。

第三章,为二分类构建深度神经网络,演示了如何在 DL4J 中开发深度神经网络,以解决二分类问题。

第四章,构建卷积神经网络,解释了如何在 DL4J 中开发卷积神经网络,以解决图像分类问题。

第五章,实现自然语言处理,讨论了如何使用 DL4J 开发 NLP 应用。

第六章,构建 LSTM 网络进行时间序列分析,展示了一个基于 PhysioNet 数据集的时间序列应用,使用 DL4J 进行单类别输出。

第七章,构建 LSTM 神经网络进行序列分类,展示了一个基于 UCI 合成控制数据集的时间序列应用,使用 DL4J 进行多类别输出。

第八章,对无监督数据进行异常检测,解释了如何使用 DL4J 开发一个无监督的异常检测应用。

第九章,使用 RL4J 进行强化学习,解释了如何开发一个强化学习代理,使其能够使用 RL4J 学习玩Malmo游戏。

第十章,在分布式环境中开发应用,讲解如何使用 DL4J 开发分布式深度学习应用。

第十一章,将迁移学习应用于网络模型,展示了如何将迁移学习应用于 DL4J 应用。

第十二章,基准测试与神经网络优化,讨论了各种基准测试方法和可以应用于深度学习应用的神经网络优化技术。

为了最大化地利用本书

读者应具备基本的深度学习、强化学习和数据分析知识。基本的深度学习知识有助于理解神经网络设计和示例中使用的各种超参数。基本的数据分析技能和对数据要求的理解将有助于你更好地探索 DataVec,而一些强化学习基础知识则会在你阅读第九章,使用 RL4J 进行强化学习时提供帮助。我们还将在第十章,在分布式环境中开发应用中讨论分布式神经网络,建议具备 Apache Spark 的基础知识。

下载示例代码文件

你可以从你在www.packt.com的账户下载本书的示例代码文件。如果你在其他地方购买了本书,你可以访问www.packtpub.com/support,注册后直接通过邮件获取文件。

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

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

  2. 选择 Support 选项卡。

  3. 点击“代码下载”。

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

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

  • WinRAR/7-Zip for Windows

  • Mac 的 Zipeg/iZip/UnRarX

  • Linux 的 7-Zip/PeaZip

本书的代码包也托管在 GitHub 上:github.com/PacktPublishing/Java-Deep-Learning-Cookbook。如果代码有更新,将会在现有的 GitHub 仓库中进行更新。

我们还提供了来自我们丰富书籍和视频目录的其他代码包,大家可以访问github.com/PacktPublishing/来查看!

下载彩色图片

我们还提供了一个 PDF 文件,里面包含本书中使用的屏幕截图/图表的彩色图片。你可以在此处下载:static.packt-cdn.com/downloads/9781788995207_ColorImages.pdf

使用的约定

本书中有许多文本约定。

CodeInText:表示文本中的代码词汇、数据库表名、文件夹名称、文件名、文件扩展名、路径名、虚拟网址、用户输入和 Twitter 用户名。示例:“创建一个CSVRecordReader来保存客户流失数据。”

代码块的格式如下:

File file = new File("Churn_Modelling.csv");
recordReader.initialize(new FileSplit(file)); 

所有命令行输入或输出的格式如下:

mvn clean install

粗体:表示新术语、重要词汇或屏幕上出现的文字。例如,菜单或对话框中的文字会在文本中以这种方式出现。示例:“我们只需要点击左侧边栏上的 Model 选项卡。”

警告或重要说明如下所示。

提示和技巧如下所示。

各部分

在本书中,你会看到几个常出现的标题(准备工作如何操作...工作原理...还有更多...,以及另见)。

为了清晰地说明如何完成食谱,请按以下方式使用这些部分:

准备工作

本节告诉你在食谱中需要预期的内容,并描述如何设置食谱所需的软件或任何预先设置。

如何操作…

本节包含遵循食谱所需的步骤。

工作原理…

本节通常包括对上一节所做事情的详细说明。

还有更多…

本节包含有关食谱的附加信息,以帮助你对该食谱有更深入的了解。

另见

本节提供了有关该食谱的其他有用信息的链接。

联系我们

我们总是欢迎读者的反馈。

一般反馈:如果你对本书的任何方面有疑问,请在邮件主题中提及书名,并通过customercare@packtpub.com联系我们。

勘误:尽管我们已尽力确保内容的准确性,但仍可能出现错误。如果您在本书中发现错误,我们将非常感激您向我们报告。请访问 www.packtpub.com/support/errata,选择您的书籍,点击“勘误提交表单”链接并填写详细信息。

盗版:如果您在互联网上遇到任何形式的非法复制品,我们将非常感激您提供其位置地址或网站名称。请通过copyright@packt.com与我们联系,并附上相关材料的链接。

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

评价

请留下评价。阅读并使用完本书后,何不在您购买书籍的网站上留下评价?潜在读者可以参考您的公正意见做出购买决策,我们 Packt 可以了解您对我们产品的看法,我们的作者也能看到您对其书籍的反馈。谢谢!

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

第一章:Java 中的深度学习简介

让我们讨论一下各种深度学习库,以便选择最适合当前任务的库。这是一个依赖于具体情境的决策,会根据情况有所不同。在本章中,我们将首先简要介绍深度学习,并探讨为什么 DL4J 是解决深度学习问题的一个好选择。我们还将讨论如何在工作空间中设置 DL4J。

在本章中,我们将涵盖以下内容:

  • 深度学习的直觉

  • 确定解决深度学习问题的正确网络类型

  • 确定正确的激活函数

  • 克服过拟合问题

  • 确定正确的批处理大小和学习率

  • 配置 Maven 以支持 DL4J

  • 配置 DL4J 以支持 GPU 加速环境

  • 解决安装问题

技术要求

要充分利用这本食谱,你需要以下内容:

  • 安装 Java SE 7 或更高版本

  • 基本的 Java 核心知识

  • DL4J 基础

  • Maven 基础

  • 基本的数据分析技能

  • 深度学习/机器学习基础

  • 操作系统命令基础(Linux/Windows)

  • IntelliJ IDEA IDE(这是管理代码的非常简便和无烦恼的方法;不过,你也可以尝试其他 IDE,如 Eclipse)

  • Spring Boot 基础(将 DL4J 与 Spring Boot 集成,用于 Web 应用)

本书中我们使用的是 DL4J 版本 1.0.0-beta3,除了第七章《构建 LSTM 神经网络进行序列分类》外,在那里我们使用了最新版本 1.0.0-beta4,以避免出现 BUG。

深度学习的直觉

如果你是深度学习的新手,可能会好奇它到底与机器学习有什么不同;还是一样的?深度学习是机器学习这个大领域的一个子集。让我们以汽车图像分类问题为例来思考这个问题:

正如你在前面的图示中看到的,我们需要自行执行特征提取,因为传统的机器学习算法无法自行完成这些工作。它们可能在准确性上非常高效,但无法从数据中学习信号。事实上,它们并不会自己学习,仍然依赖于人类的努力:

另一方面,深度学习算法通过自我学习来执行任务。神经网络的工作原理基于深度学习的概念,它们通过自身训练来优化结果。然而,最终的决策过程是隐藏的,无法追踪。深度学习的目标是模仿人类大脑的工作方式。

反向传播

神经网络的核心是反向传播算法。请参阅下图所示的示例神经网络结构:

对于任何神经网络,在前向传播过程中,数据从输入层流向输出层。图中的每个圆圈代表一个神经元。每一层都有若干个神经元。我们的数据将在各层之间的神经元中流动。输入需要是数值格式,以支持神经元中的计算操作。每个神经元都分配有一个权重(矩阵)和一个激活函数。通过输入数据、权重矩阵和激活函数,生成每个神经元的概率值。通过损失函数,在输出层计算误差(即与实际值的偏差)。我们在反向传播过程中利用损失值(即从输出层到输入层),通过重新分配权重给神经元来减少损失值。在这个阶段,一些输出层的神经元会分配较高的权重,反之亦然,这取决于损失值的结果。这个过程将一直向后推进,直到输入层,通过更新神经元的权重。在简言之,我们正在追踪损失相对于神经元权重变化的变化率。整个过程(前向传播和反向传播)被称为一个周期(epoch)。在训练过程中,我们会进行多个周期。神经网络会在每个训练周期后优化结果。

多层感知器(MLP)

多层感知器(MLP)是一个标准的前馈神经网络,至少有三层:输入层、隐藏层和输出层。隐藏层在结构中位于输入层之后。深度神经网络在结构中有两层或更多的隐藏层,而 MLP 只有一层。

卷积神经网络(CNN)

卷积神经网络通常用于图像分类问题,但由于其良好的效果,也可以应用于自然语言处理NLP),与词向量结合使用。与普通神经网络不同,CNN 会有额外的层,如卷积层和子采样层。卷积层接受输入数据(如图像),并在其上执行卷积操作。你可以把它理解为对输入应用一个函数。卷积层充当过滤器,将感兴趣的特征传递给下一个子采样层。感兴趣的特征可以是任何东西(例如,在图像的情况下,可以是毛发、阴影等),这些特征可以用来识别图像。在子采样层,来自卷积层的输入会被进一步平滑处理。因此,我们最终得到的是一个分辨率较小、色彩对比度较低的图像,保留了重要的信息。然后,输入会传递给全连接层。全连接层类似于常规的前馈神经网络。

循环神经网络(RNN)

RNN 是一个能够处理序列数据的神经网络。在常规的前馈神经网络中,当前的输入仅对下一个层的神经元有影响。相反,RNN 不仅可以接受当前的输入,还能接受先前的输入。它还可以利用内存来记忆之前的输入。因此,它能够在整个训练过程中保持长期依赖关系。RNN 在自然语言处理任务中,特别是在语音识别中非常流行。在实践中,稍作变化的结构长短期记忆网络LSTM)常常作为 RNN 的更好替代方案。

为什么 DL4J 对深度学习如此重要?

以下几点将帮助你理解为什么 DL4J 在深度学习中如此重要:

  • DL4J 提供商业支持。它是第一个商用级别的、开源的、Java 中的深度学习库。

  • 编写训练代码简单而精确。DL4J 支持即插即用模式,这意味着在硬件之间切换(从 CPU 到 GPU)只需修改 Maven 依赖项,无需更改代码。

  • DL4J 使用 ND4J 作为其后端。ND4J 是一个计算库,在大规模矩阵运算中,其速度比 NumPy(Python 中的计算库)快两倍。与其他 Python 库相比,DL4J 在 GPU 环境下展现出更快的训练速度。

  • DL4J 支持在使用 Apache Spark 的集群机器上进行训练,无论是 CPU 还是 GPU。DL4J 引入了分布式训练中的自动并行化。这意味着 DL4J 通过设置工作节点和连接,绕过了额外库的需求。

  • DL4J 是一个非常适合生产的深度学习库。作为一个基于 JVM 的库,DL4J 应用可以轻松与现有的运行在 Java/Scala 中的企业应用集成或部署。

确定解决深度学习问题的正确网络类型

识别正确的神经网络类型对于高效解决业务问题至关重要。标准的神经网络对于大多数用例而言是最佳选择,并能产生近似的结果。然而,在某些场景下,核心神经网络架构需要进行修改,以便适应特征(输入)并产生所需的结果。在下面的实例中,我们将通过已知用例的帮助,逐步介绍如何为深度学习问题选择最佳的网络架构。

如何实现...

  1. 确定问题类型。

  2. 确定系统中所涉及的数据类型。

它是如何工作的...

为了有效地解决用例,我们需要通过确定问题类型来使用正确的神经网络架构。以下是一些全球性的用例及其相应的 problem 类型,供第一步参考:

  • 欺诈检测问题:我们希望区分合法和可疑交易,以便从整个交易列表中分离出异常活动。目标是减少假阳性(即错误地将合法交易标记为欺诈)案例。因此,这是一个异常检测问题。

  • 预测问题:预测问题可以是分类问题或回归问题。对于标记的分类数据,我们可以有离散标签。我们需要针对这些离散标签对数据进行建模。另一方面,回归模型没有离散标签。

  • 推荐问题:你需要构建一个推荐系统(推荐引擎)来向客户推荐产品或内容。推荐引擎还可以应用于执行任务的代理,例如游戏、自动驾驶、机器人运动等。推荐引擎实施强化学习,并且通过引入深度学习可以进一步增强。

我们还需要知道神经网络所消耗的数据类型。以下是一些使用案例及其相应的数据类型,适用于步骤 2:

  • 欺诈检测问题:交易通常发生在若干时间步骤中。因此,我们需要持续收集交易数据。这是一个时间序列数据的例子。每个时间序列代表一组新的交易记录。这些时间序列可以是规则的或不规则的。例如,如果你有信用卡交易数据需要分析,那么你拥有的是有标签的数据。如果是来自生产日志的用户元数据,则可能是无标签数据。我们可以有用于欺诈检测分析的有监督/无监督数据集,例如。看看以下的 CSV 有监督数据集:

在前面的截图中,像amountoldBalanceOrg等特征是有意义的,每个记录都有一个标签,指示该观察是否为欺诈。

另一方面,无监督数据集不会为你提供任何关于输入特征的线索。它也没有任何标签,如下所示的 CSV 数据所示:

如你所见,特征标签(顶部行)遵循一个编号命名规范,但没有任何关于其对欺诈检测结果重要性的提示。我们还可以拥有时间序列数据,其中交易记录会在一系列时间步骤中被记录。

  • 预测问题:从组织收集的历史数据可以用来训练神经网络。这些数据通常是简单的文件类型,如 CSV/text 文件。数据可以作为记录获得。对于股市预测问题,数据类型将是时间序列。狗品种预测问题需要提供狗的图片来训练网络。股票价格预测是回归问题的一个例子。股票价格数据集通常是时间序列数据,其中股价在一系列时间步骤中被记录,如下所示:

在大多数股票价格数据集中,包含多个文件。每个文件代表一个公司股市。每个文件都记录了股价在一系列时间步骤中的变化,如此处所示:

  • 推荐问题:对于产品推荐系统,显性数据可能是网站上发布的客户评价,而隐性数据可能是客户活动历史,例如产品搜索或购买历史。我们将使用未标注的数据来馈送神经网络。推荐系统还可以解决游戏问题或学习需要技能的工作。代理(在强化学习过程中训练以执行任务)可以通过图像帧或任何文本数据(无监督)实时获取数据,根据其状态学习该采取何种行动。

还有更多...

以下是可能的深度学习解决方案,用于之前讨论的不同问题类型:

  • 欺诈检测问题:最优解决方案根据数据的不同而有所变化。我们之前提到了两种数据源。一种是信用卡交易,另一种是基于用户登录/注销活动的元数据。在第一种情况下,我们有标注数据,并且有交易序列需要分析。

循环神经网络可能最适合处理序列数据。你可以添加 LSTM(deeplearning4j.org/api/latest/org/deeplearning4j/nn/layers/recurrent/LSTM.html)循环层,DL4J 也有相应的实现。对于第二种情况,我们有未标注的数据,最佳选择是变分自编码器(deeplearning4j.org/api/latest/org/deeplearning4j/nn/layers/variational/VariationalAutoencoder.html)来压缩未标注数据。

  • 预测问题:对于使用 CSV 记录的分类问题,前馈神经网络足够了。对于时间序列数据,最佳选择是循环神经网络,因为数据具有序列性。对于图像分类问题,你需要使用 CNN(deeplearning4j.org/api/latest/org/deeplearning4j/nn/conf/layers/ConvolutionLayer.Builder.html)

  • 推荐问题:我们可以使用强化学习RL)来解决推荐问题。强化学习通常用于此类应用场景,并且可能是更好的选择。RL4J 是专为此目的开发的。我们将在第九章中介绍 RL4J,使用 RL4J 进行强化学习,因为此时它将是一个较为高级的话题。我们还可以选择更简单的选项,如前馈神经网络(RNN)并采用不同的方法。我们可以将未标注的数据序列馈送到循环神经网络或卷积层,具体根据数据类型(图像/文本/视频)。一旦推荐的内容/产品被分类,你可以应用进一步的逻辑,从列表中根据客户偏好随机拉取产品。

为了选择合适的网络类型,你需要了解数据的类型以及它试图解决的问题。你可以构建的最基本的神经网络是前馈网络或多层感知器。你可以在 DL4J 中使用NeuralNetConfiguration来创建多层网络架构。

请参阅以下在 DL4J 中的神经网络配置示例:

MultiLayerConfiguration configuration = new NeuralNetConfiguration.Builder()
 .weightInit(WeightInit.RELU_UNIFORM)
 .updater(new Nesterovs(0.008,0.9))
 .list()
 .layer(new DenseLayer.Builder().nIn(layerOneInputNeurons).nOut(layerOneOutputNeurons).activation(Activation.RELU).dropOut(dropOutRatio).build())
 .layer(new DenseLayer.Builder().nIn(layerTwoInputNeurons).nOut(layerTwoOutputNeurons).activation(Activation.RELU).dropOut(0.9).build())
 .layer(new OutputLayer.Builder(new LossMCXENT(weightsArray))
 .nIn(layerThreeInputNeurons).nOut(numberOfLabels).activation(Activation.SOFTMAX).build())
 .backprop(true).pretrain(false)
 .build();

我们为神经网络中的每一层指定激活函数,nIn()nOut()表示神经元层的输入/输出连接数。dropOut()函数的目的是优化网络性能。我们在第三章中提到过这一点,构建用于二分类的深度神经网络。本质上,我们通过随机忽略一些神经元来避免在训练过程中盲目记忆模式。激活函数将在本章的确定合适的激活函数部分讨论。其他属性则控制神经元之间权重的分配以及如何处理每个周期计算出的误差。

让我们专注于一个具体的决策过程:选择合适的网络类型。有时,使用自定义架构会得到更好的结果。例如,你可以使用词向量结合 CNN 来执行句子分类。DL4J 提供了ComputationGraph(deeplearning4j.org/api/latest/org/deeplearning4j/nn/graph/ComputationGraph.html)实现,以适应 CNN 架构。

ComputationGraph允许任意(自定义)神经网络架构。以下是它在 DL4J 中的定义:

public ComputationGraph(ComputationGraphConfiguration configuration) {
 this.configuration = configuration;
 this.numInputArrays = configuration.getNetworkInputs().size();
 this.numOutputArrays = configuration.getNetworkOutputs().size();
 this.inputs = new INDArray[numInputArrays];
 this.labels = new INDArray[numOutputArrays];
 this.defaultConfiguration = configuration.getDefaultConfiguration();//Additional source is omitted from here. Refer to https://github.com/deeplearning4j/deeplearning4j
}

实现 CNN 就像为前馈网络构建网络层一样:

public class ConvolutionLayer extends FeedForwardLayer

一个 CNN 除了DenseLayerOutputLayer外,还包括ConvolutionalLayerSubsamplingLayer

确定合适的激活函数

激活函数的目的是引入非线性到神经网络中。非线性有助于神经网络学习更复杂的模式。我们将讨论一些重要的激活函数及其相应的 DL4J 实现。

以下是我们将考虑的激活函数:

  • Tanh

  • Sigmoid

  • ReLU(修正线性单元的简称)

  • Leaky ReLU

  • Softmax

在本食谱中,我们将介绍决定神经网络激活函数的关键步骤。

如何实现...

  1. 根据网络层选择激活函数:我们需要知道输入/隐藏层和输出层使用的激活函数。最好为输入/隐藏层使用 ReLU。

  2. 选择合适的激活函数来处理数据杂质:检查馈送给神经网络的数据。你是否有大部分为负值的输入,导致死神经元出现?根据情况选择合适的激活函数。如果在训练中观察到死神经元,请使用 Leaky ReLU。

  3. 选择合适的激活函数以应对过拟合:观察每个训练周期的评估指标及其变化。理解梯度行为以及模型在新未见数据上的表现。

  4. 根据预期输出选择合适的激活函数:首先检查网络的期望结果。例如,当你需要衡量输出类别发生的概率时,可以使用 SOFTMAX 函数。它通常用于输出层。对于任何输入/隐藏层,大多数情况下你需要使用 ReLU。如果不确定该使用哪种激活函数,可以先尝试使用 ReLU;如果它没有改善你的期望,再尝试其他激活函数。

它是如何工作的...

对于第 1 步,ReLU 因其非线性行为而被广泛使用。输出层的激活函数取决于期望的输出行为。第 4 步也针对这一点。

对于第 2 步,Leaky ReLU 是 ReLU 的改进版本,用于避免零梯度问题。然而,可能会观察到性能下降。如果在训练过程中观察到死神经元,我们会使用 Leaky ReLU。死神经元指的是对于所有可能的输入,其梯度为零的神经元,这使得它们在训练中没有任何作用。

对于第 3 步,tanh 和 sigmoid 激活函数是相似的,通常用于前馈网络。如果你使用这些激活函数,请确保对网络层进行正则化,以避免梯度消失问题。这些激活函数通常用于分类问题。

还有更多...

ReLU 激活函数是非线性的,因此,误差的反向传播可以轻松进行。反向传播是神经网络的核心算法。这是一个学习算法,通过计算神经元权重的梯度下降来更新神经网络。以下是目前在 DL4J 中支持的 ReLU 变体:

  • ReLU: 标准的 ReLU 激活函数:
public static final Activation RELU
  • ReLU6: ReLU 激活函数,其输出最大值为 6,6 是一个任意选择:
public static final Activation RELU6
  • RReLU: 随机化的 ReLU 激活函数:
public static final Activation RRELU
  • ThresholdedReLU: 阈值 ReLU:
public static final Activation THRESHOLDEDRELU

还有一些其他实现,比如SeLU缩放指数线性单元的缩写),它与 ReLU 激活函数类似,但对于负值有一个斜率。

应对过拟合问题

正如我们所知,过拟合是机器学习开发者面临的主要挑战。当神经网络架构复杂且训练数据庞大时,过拟合问题尤为严重。提到过拟合时,我们并没有忽视欠拟合的可能性。我们将过拟合和欠拟合放在同一个类别中讨论。让我们讨论一下如何应对过拟合问题。

可能导致过拟合的原因包括但不限于:

  • 特征变量的数量相对于数据记录的数量过多。

  • 一个复杂的神经网络模型

显而易见,过拟合会降低网络的泛化能力,当发生过拟合时,网络将会拟合噪声而不是信号。在这个方案中,我们将介绍预防过拟合问题的关键步骤。

如何操作……

  1. 使用KFoldIterator进行基于 k 折交叉验证的重采样:
KFoldIterator kFoldIterator = new KFoldIterator(k, dataSet);
  1. 构建一个更简单的神经网络架构。

  2. 使用足够的训练数据来训练神经网络。

它是如何工作的……

在步骤 1 中,k是任意选择的数字,而dataSet是代表训练数据的数据集对象。我们执行 k 折交叉验证来优化模型评估过程。

复杂的神经网络架构可能导致网络倾向于记忆模式。因此,神经网络将很难对未见过的数据进行泛化。例如,拥有少量的隐藏层要比拥有数百个隐藏层更好、更高效。这就是步骤 2 的相关性。

相对较大的训练数据将鼓励网络更好地学习,而按批次评估测试数据将增加网络的泛化能力。这就是步骤 3 的相关性。尽管在 DL4J 中有多种类型的数据迭代器和不同方式引入批量大小,但以下是RecordReaderDataSetIterator的更常规定义:

public RecordReaderDataSetIterator(RecordReader recordReader,
 WritableConverter converter,
 int batchSize,
 int labelIndexFrom,
 int labelIndexTo,
 int numPossibleLabels,
 int maxNumBatches,
 boolean regression)

还有更多……

当你执行 k 折交叉验证时,数据被划分为k个子集。对于每个子集,我们通过将其中一个子集用于测试,剩余的k-1个子集用于训练来进行评估。我们将重复执行这一过程k次。实际上,我们使用所有数据进行训练,而不会丢失任何数据,这与浪费部分数据进行测试是不同的。

这里处理了欠拟合问题。然而,请注意,我们仅执行k次评估。

当你进行批量训练时,整个数据集会根据批量大小进行划分。如果你的数据集有 1,000 条记录,而批量大小是 8,那么你将有 125 个训练批次。

你还需要注意训练与测试的比例。根据这个比例,每个批次将被划分为训练集和测试集。然后,评估将根据此进行。对于 8 折交叉验证,你评估模型 8 次,但对于批量大小为 8,你将进行 125 次模型评估。

注意这里严格的评估模式,这将有助于提高泛化能力,同时增加欠拟合的几率。

确定正确的批量大小和学习率

尽管没有适用于所有模型的特定批量大小或学习率,我们可以通过尝试多个训练实例来找到它们的最佳值。首要步骤是通过模型尝试一组批量大小值和学习率。通过评估额外的参数如PrecisionRecallF1 Score来观察模型的效率。仅仅测试分数并不能确认模型的性能。同时,诸如PrecisionRecallF1 Score这些参数根据使用情况会有所不同。您需要分析您的问题陈述以了解这一点。在这个示例中,我们将介绍确定正确批量大小和学习率的关键步骤。

如何操作...

  1. 多次运行训练实例并跟踪评估指标。

  2. 通过增加学习率运行实验并跟踪结果。

工作原理如下...

考虑以下实验以说明第 1 步。

使用批量大小为 8 和学习率为 0.008 对 10,000 条记录进行了以下训练:

对相同数据集进行了批量大小为 50 和学习率为 0.008 的评估:

为了执行第 2 步,我们将学习率增加到 0.6,以观察结果。请注意,超过一定限制的学习率将不会以任何方式提高效率。我们的任务是找到这个限制:

您可以观察到Accuracy降低到 82.40%,F1 Score降低到 20.7%。这表明F1 Score可能是该模型中需要考虑的评估参数。这并不适用于所有模型,我们在尝试了几个批量大小和学习率后得出这一结论。简而言之,您必须重复相同的训练过程,并选择能产生最佳结果的任意值。

还有更多内容...

当我们增加批量大小时,迭代次数最终会减少,因此评估次数也会减少。这可能会导致对于大批量大小的数据过拟合。批量大小为 1 与基于整个数据集的批量大小一样无效。因此,您需要从一个安全的任意点开始尝试不同的值。

非常小的学习率将导致收敛速度非常缓慢,这也会影响训练时间。如果学习率非常大,这将导致模型的发散行为。我们需要增加学习率,直到观察到评估指标变得更好。fast.ai 和 Keras 库中有循环学习率的实现;然而,在 DL4J 中并没有实现循环学习率。

配置 Maven 以用于 DL4J。

我们需要添加 DL4J/ND4J Maven 依赖项以利用 DL4J 的功能。ND4J 是专为 DL4J 设计的科学计算库。必须在你的 pom.xml 文件中提及 ND4J 后端依赖项。在本示例中,我们将在 pom.xml 中添加一个特定于 CPU 的 Maven 配置。

准备工作。

让我们讨论所需的 Maven 依赖项。我们假设您已经完成了以下工作:

  • 已安装 JDK 1.7 或更高版本,并设置了 PATH 变量。

  • Maven 已安装并且 PATH 变量已设置。

运行 DL4J 需要 64 位 JVM。

设置 JDK 和 Maven 的 PATH 变量:

  • 在 Linux 上:使用 export 命令将 Maven 和 JDK 添加到 PATH 变量中:
export PATH=/opt/apache-maven-3.x.x/bin:$PATH
export PATH=${PATH}:/usr/java/jdk1.x.x/bin

根据安装更换版本号。

  • 在 Windows 上:从系统属性设置系统环境变量:
set PATH="C:/Program Files/Apache Software Foundation/apache-maven-3.x.x/bin:%PATH%"
 set PATH="C:/Program Files/Java/jdk1.x.x/bin:%PATH%"

根据安装更换 JDK 版本号。

如何做到……

  1. 添加 DL4J 核心依赖项:
<dependency>
 <groupId>org.deeplearning4j</groupId>
 <artifactId>deeplearning4j-core</artifactId>
 <version>1.0.0-beta3</version>
 </dependency>

  1. 添加 ND4J 本地依赖项:
<dependency>
 <groupId>org.nd4j</groupId>
 <artifactId>nd4j-native-platform</artifactId>
 <version>1.0.0-beta3</version>
 </dependency>

  1. 添加 DataVec 依赖项以执行 ETL(提取、转换和加载)操作:
<dependency>
 <groupId>org.datavec</groupId>
 <artifactId>datavec-api</artifactId>
 <version>1.0.0-beta3</version>
 </dependency>
  1. 启用日志以进行调试:
<dependency>
 <groupId>org.slf4j</groupId>
 <artifactId>slf4j-simple</artifactId>
 <version>1.7.25</version> //change to latest version
 </dependency> 

请注意,在编写本书时,1.0.0-beta 3 是当前 DL4J 的发布版本,并且是本食谱中使用的官方版本。此外,请注意 DL4J 依赖于一个 ND4J 后端用于硬件特定的实现。

工作原理……

在添加了 DL4J 核心依赖项和 ND4J 依赖项后,正如步骤 1 和步骤 2 中所述,我们能够创建神经网络。在步骤 2 中,ND4J Maven 配置被提及为 Deeplearnign4j 必要的后端依赖项。ND4J 是 Deeplearning4j 的科学计算库。

ND4J 是为 Java 编写的科学计算库,就像 NumPy 是为 Python 编写的一样。

步骤 3 对于 ETL 过程非常关键:即数据提取、转换和加载。因此,我们在使用数据训练神经网络时肯定需要它。

步骤 4 是可选的,但建议进行,因为记录日志将减少调试所需的工作量。

配置 DL4J 以用于 GPU 加速环境。

对于 GPU 驱动硬件,DL4J 提供了不同的 API 实现。这是为了确保有效利用 GPU 硬件,而不浪费硬件资源。资源优化是生产中昂贵的 GPU 应用程序的主要关注点。在本示例中,我们将向 pom.xml 中添加一个特定于 GPU 的 Maven 配置。

准备工作。

您将需要以下内容才能完成此食谱:

  • JDK 版本为 1.7 或更高,并已安装并添加到 PATH 变量中。

  • Maven 已安装并添加到 PATH 变量。

  • 兼容 NVIDIA 硬件。

  • CUDA v9.2+ 已安装并配置。

  • cuDNN(CUDA 深度神经网络)已安装并配置。

如何做到……

  1. 从 NVIDIA 开发者网站 URL:developer.nvidia.com/cuda-downloads 下载并安装 CUDA v9.2+。

  2. 配置 CUDA 依赖项。对于 Linux,打开终端并编辑.bashrc文件。运行以下命令,并确保根据你的下载版本替换用户名和 CUDA 版本号:

nano /home/username/.bashrc
 export PATH=/usr/local/cuda-9.2/bin${PATH:+:${PATH}}$

 export LD_LIBRARY_PATH=/usr/local/cuda-9.2/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}

 source .bashrc

  1. 对于旧版本的 DL4J,需将lib64目录添加到PATH中。

  2. 运行nvcc --version命令以验证 CUDA 的安装。

  3. 添加 ND4J CUDA 后端的 Maven 依赖项:

<dependency>
 <groupId>org.nd4j</groupId>
 <artifactId>nd4j-cuda-9.2</artifactId>
 <version>1.0.0-beta3</version>
 </dependency> 
  1. 添加 DL4J CUDA Maven 依赖项:
<dependency>
 <groupId>org.deeplearning4j</groupId>
 <artifactId>deeplearning4j-cuda-9.2</artifactId>
 <version>1.0.0-beta3</version>
 </dependency> 
  1. 添加 cuDNN 依赖项,以使用捆绑的 CUDA 和 cuDNN:
<dependency>
 <groupId>org.bytedeco.javacpp-presets</groupId>
 <artifactId>cuda</artifactId>
 <version>9.2-7.1-1.4.2</version>
 <classifier>linux-x86_64-redist</classifier> //system specific
 </dependency>

工作原理...

我们已经通过步骤 1 到 4 配置了 NVIDIA CUDA。有关更详细的操作系统特定说明,请参考官方的 NVIDIA CUDA 网站:developer.nvidia.com/cuda-downloads

根据你的操作系统,网站上将显示安装说明。DL4J 版本 1.0.0-beta 3 目前支持 CUDA 安装版本 9.0、9.2 和 10.0。例如,如果你需要为 Ubuntu 16.04 安装 CUDA v10.0,你应按照以下步骤访问 CUDA 网站:

请注意,步骤 3 不适用于 DL4J 的较新版本。对于 1.0.0-beta 及更高版本,所需的 CUDA 库已经与 DL4J 捆绑在一起。然而,这不适用于步骤 7。

此外,在继续执行步骤 5 和 6 之前,请确保pom.xml中没有冗余的依赖项(如 CPU 专用依赖项)。

DL4J 支持 CUDA,但通过添加 cuDNN 库可以进一步加速性能。cuDNN 不会作为捆绑包出现在 DL4J 中。因此,请确保从 NVIDIA 开发者网站下载并安装 NVIDIA cuDNN。安装并配置完 cuDNN 后,我们可以按照步骤 7 在 DL4J 应用程序中添加对 cuDNN 的支持。

还有更多...

对于多 GPU 系统,你可以通过在应用程序的主方法中加入以下代码来消耗所有 GPU 资源:

CudaEnvironment.getInstance().getConfiguration().allowMultiGPU(true);

这是一个临时的解决方法,用于在多 GPU 硬件的情况下初始化 ND4J 后端。通过这种方式,如果有更多的 GPU 可用,我们将不会只限于少量的 GPU 资源。

排查安装问题

虽然 DL4J 的设置看起来不复杂,但由于操作系统或系统中安装的应用程序等因素,仍然可能会遇到安装问题。CUDA 安装问题不在本书的范围内。由于未解析的依赖关系引起的 Maven 构建问题可能有多种原因。如果你在一个有自己内部仓库和代理的组织中工作,那么你需要在pom.xml文件中进行相关更改。这些问题也不在本书的范围内。在本篇教程中,我们将逐步介绍如何解决 DL4J 常见的安装问题。

准备就绪

在继续之前,必须进行以下检查:

  • 验证 Java 和 Maven 是否已安装,并且PATH变量已配置。

  • 验证 CUDA 和 cuDNN 的安装。

  • 验证 Maven 构建是否成功,并且依赖项是否已下载到 ~/.m2/repository

如何操作...

  1. 启用日志级别以提供更多关于错误的信息:
Logger log = LoggerFactory.getLogger("YourClassFile.class");
 log.setLevel(Level.DEBUG);
  1. 验证 JDK/Maven 的安装和配置。

  2. 检查是否所有必要的依赖项都已添加到 pom.xml 文件中。

  3. 删除 Maven 本地仓库的内容并重新构建 Maven,以减轻 DL4J 中的 NoClassDefFoundError。在 Linux 中,操作如下:

rm -rf ~/.m2/repository/org/deeplearning4j
 rm -rf ~/.m2/repository/org/datavec
 mvn clean install
  1. 减少在 DL4J 中出现 ClassNotFoundException。如果第 4 步没有帮助解决问题,您可以尝试此方法。DL4J/ND4J/DataVec 应该使用相同的版本。对于与 CUDA 相关的错误堆栈,请检查安装是否正常。

如果添加适当的 DL4J CUDA 版本仍然无法解决问题,请检查您的 cuDNN 安装。

它是如何工作的...

为了减少如 ClassNotFoundException 之类的异常,主要任务是验证我们是否正确安装了 JDK(第 2 步),以及我们设置的环境变量是否指向正确的位置。第 3 步也很重要,因为缺失的依赖项会导致相同的错误。

在第 4 步中,我们删除本地仓库中冗余的依赖项,并尝试重新构建 Maven。以下是尝试运行 DL4J 应用程序时出现 NoClassDefFoundError 的示例:

root@instance-1:/home/Deeplearning4J# java -jar target/dl4j-1.0-SNAPSHOT.jar
 09:28:22.171 [main] INFO org.nd4j.linalg.factory.Nd4jBackend - Loaded [JCublasBackend] backend
 Exception in thread "main" java.lang.NoClassDefFoundError: org/nd4j/linalg/api/complex/IComplexDouble
 at java.lang.Class.forName0(Native Method)
 at java.lang.Class.forName(Class.java:264)
 at org.nd4j.linalg.factory.Nd4j.initWithBackend(Nd4j.java:5529)
 at org.nd4j.linalg.factory.Nd4j.initContext(Nd4j.java:5477)
 at org.nd4j.linalg.factory.Nd4j.(Nd4j.java:210)
 at org.datavec.image.transform.PipelineImageTransform.(PipelineImageTransform.java:93)
 at org.datavec.image.transform.PipelineImageTransform.(PipelineImageTransform.java:85)
 at org.datavec.image.transform.PipelineImageTransform.(PipelineImageTransform.java:73)
 at examples.AnimalClassifier.main(AnimalClassifier.java:72)
 Caused by: java.lang.ClassNotFoundException: org.nd4j.linalg.api.complex.IComplexDouble

NoClassDefFoundError 的一个可能原因是 Maven 本地仓库中缺少必要的依赖项。因此,我们删除仓库内容并重新构建 Maven 以重新下载依赖项。如果由于中断导致某些依赖项未被下载,现在应该会重新下载。

这是一个在 DL4J 训练期间遇到 ClassNotFoundException 的示例:

再次提醒,这可能是版本问题或冗余依赖导致的。

还有更多...

除了前面讨论的常见运行时问题,Windows 用户在训练 CNN 时可能会遇到 cuDNN 特定的错误。实际的根本原因可能不同,通常标记为 UnsatisfiedLinkError

o.d.n.l.c.ConvolutionLayer - Could not load CudnnConvolutionHelper
 java.lang.UnsatisfiedLinkError: no jnicudnn in java.library.path
 at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867) ~[na:1.8.0_102]
 at java.lang.Runtime.loadLibrary0(Runtime.java:870) ~[na:1.8.0_102]
 at java.lang.System.loadLibrary(System.java:1122) ~[na:1.8.0_102]
 at org.bytedeco.javacpp.Loader.loadLibrary(Loader.java:945) ~[javacpp-1.3.1.jar:1.3.1]
 at org.bytedeco.javacpp.Loader.load(Loader.java:750) ~[javacpp-1.3.1.jar:1.3.1]
 Caused by: java.lang.UnsatisfiedLinkError: C:\Users\Jürgen.javacpp\cache\cuda-7.5-1.3-windows-x86_64.jar\org\bytedeco\javacpp\windows-x86_64\jnicudnn.dll: Can't find dependent libraries
 at java.lang.ClassLoader$NativeLibrary.load(Native Method) ~[na:1.8.0_102]

执行以下步骤来修复该问题:

  1. 在此下载最新的依赖关系检查器:github.com/lucasg/Dependencies/

  2. 将以下代码添加到您的 DL4J main() 方法中:

try {
 Loader.load(<module>.class);
 } catch (UnsatisfiedLinkError e) {
 String path = Loader.cacheResource(<module>.class, "windows-x86_64/jni<module>.dll").getPath();
 new ProcessBuilder("c:/path/to/DependenciesGui.exe", path).start().waitFor();
 }
  1. <module> 替换为遇到问题的 JavaCPP 预设模块的名称;例如,cudnn。对于较新的 DL4J 版本,必要的 CUDA 库已经与 DL4J 打包。因此,您不应该再遇到此问题。

如果您觉得可能发现了 DL4J 的 bug 或功能错误,欢迎在 github.com/eclipse/deeplearning4j 上创建问题跟踪。

您也可以在这里与 Deeplearning4j 社区进行讨论:gitter.im/deeplearning4j/deeplearning4j

第二章:数据提取、转换和加载

让我们讨论任何机器学习难题中最重要的部分:数据预处理和规范化。垃圾进,垃圾出是最合适的描述。在这种情况下,我们让更多噪声通过,就会得到更多不希望的输出。因此,你需要在去除噪声的同时保持信号。

另一个挑战是处理各种类型的数据。我们需要将原始数据集转换成神经网络可以理解并执行科学计算的适当格式。我们需要将数据转换成数值向量,以便网络能够理解并且可以轻松应用计算。记住,神经网络仅限于一种类型的数据:向量。

需要一种方法来加载数据到神经网络中。我们不能一次性将 100 万条数据记录放入神经网络——那样会降低性能。这里提到的性能是指训练时间。为了提高性能,我们需要利用数据管道、批量训练以及其他采样技术。

DataVec是一个输入/输出格式系统,能够管理我们刚刚提到的所有内容。它解决了每个深度学习难题所带来的最大头疼问题。DataVec支持所有类型的输入数据,如文本、图像、CSV 文件和视频。DataVec库在 DL4J 中管理数据管道。

在本章中,我们将学习如何使用DataVec执行 ETL 操作。这是构建 DL4J 神经网络的第一步。

在本章中,我们将介绍以下配方:

  • 读取和遍历数据

  • 执行模式转换

  • 序列化转换

  • 构建转换过程

  • 执行转换过程

  • 为了提高网络效率对数据进行规范化

技术要求

本章将讨论的用例的具体实现可以在github.com/PacktPublishing/Java-Deep-Learning-Cookbook/tree/master/02_Data_Extraction_Transform_and_Loading/sourceCode/cookbook-app/src/main/java/com/javadeeplearningcookbook/app找到。

克隆我们的 GitHub 仓库后,导航到Java-Deep-Learning-Cookbook/02_Data_Extraction_Transform_and_Loading/sourceCode目录。然后,将cookbook-app项目作为 Maven 项目导入,方法是导入cookbook-app目录中的pom.xml文件。

本章所需的数据集位于Chapter02根目录下(Java-Deep-Learning-Cookbook/02_Data_Extraction_Transform_and_Loading/)。你可以将其保存在不同的位置,例如你的本地目录,并在源代码中相应地引用。

读取和遍历数据

ETL 是神经网络训练中的一个重要阶段,因为它涉及到数据。在我们进行神经网络设计之前,数据的提取、转换和加载需要得到解决。糟糕的数据比效率较低的神经网络更糟糕。我们需要对以下几个方面有一个基本的了解:

  • 你尝试处理的数据类型

  • 文件处理策略

在这个配方中,我们将展示如何使用 DataVec 读取和迭代数据。

准备工作

作为前提,确保在pom.xml文件中已添加所需的 Maven 依赖项,用于 DataVec,正如我们在上一章中提到的,配置 Maven 以支持 DL4J的配方。

以下是示例pom.xml文件:github.com/rahul-raj/Java-Deep-Learning-Cookbook/blob/master/02_Data_Extraction_Transform_and_Loading/sourceCode/cookbook-app/pom.xml

如何操作...

  1. 使用FileSplit管理一系列记录:
String[] allowedFormats=new String[]{".JPEG"};
 FileSplit fileSplit = new FileSplit(new File("temp"), allowedFormats,true)

您可以在github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data%20Extraction%2C%20Transform%20and%20Loading/sourceCode/cookbook-app/src/main/java/com/javadeeplearningcookbook/app/FileSplitExample.java找到FileSplit示例。

  1. 使用CollectionInputSplit管理从文件中的 URI 集合:
FileSplit fileSplit = new FileSplit(new File("temp"));
 CollectionInputSplit collectionInputSplit = new CollectionInputSplit(fileSplit.locations());

您可以在github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data%20Extraction%2C%20Transform%20and%20Loading/sourceCode/cookbook-app/src/main/java/com/javadeeplearningcookbook/app/CollectionInputSplitExample.java找到CollectionInputSplit示例。

  1. 使用NumberedFileInputSplit来管理带有编号文件格式的数据:
NumberedFileInputSplit numberedFileInputSplit = new NumberedFileInputSplit("numberedfiles/file%d.txt",1,4);
 numberedFileInputSplit.locationsIterator().forEachRemaining(System.out::println);

你可以在github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data%20Extraction%2C%20Transform%20and%20Loading/sourceCode/cookbook-app/src/main/java/com/javadeeplearningcookbook/app/NumberedFileInputSplitExample.java找到NumberedFileInputSplit示例。

  1. 使用TransformSplit将输入 URI 映射到不同的输出 URI:
TransformSplit.URITransform uriTransform = URI::normalize;

 List<URI> uriList = Arrays.asList(new URI("file://storage/examples/./cats.txt"),
 new URI("file://storage/examples//dogs.txt"),
 new URI("file://storage/./examples/bear.txt"));

 TransformSplit transformSplit = new TransformSplit(new CollectionInputSplit(uriList),uriTransform);

你可以在github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data%20Extraction%2C%20Transform%20and%20Loading/sourceCode/cookbook-app/src/main/java/com/javadeeplearningcookbook/app/TransformSplitExample.java找到TransformSplit示例。

  1. 使用TransformSplit执行 URI 字符串替换:
InputSplit transformSplit = TransformSplit.ofSearchReplace(new CollectionInputSplit(inputFiles),"-in.csv","-out.csv");      
  1. 使用CSVRecordReader提取神经网络的 CSV 数据:
RecordReader reader = new CSVRecordReader(numOfRowsToSkip,deLimiter);
 recordReader.initialize(new FileSplit(file));

你可以在github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data%20Extraction%2C%20Transform%20and%20Loading/sourceCode/cookbook-app/src/main/java/com/javadeeplearningcookbook/app/recordreaderexamples/CSVRecordReaderExample.java找到CSVRecordReader示例。

数据集可以在github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data_Extraction_Transform_and_Loading/titanic.csv找到。

  1. 使用ImageRecordReader提取神经网络的图像数据:
ImageRecordReader imageRecordReader = new ImageRecordReader(imageHeight,imageWidth,channels,parentPathLabelGenerator);
imageRecordReader.initialize(trainData,transform);

你可以在github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data%20Extraction%2C%20Transform%20and%20Loading/sourceCode/cookbook-app/src/main/java/com/javadeeplearningcookbook/app/recordreaderexamples/ImageRecordReaderExample.java找到ImageRecordReader示例。

  1. 使用TransformProcessRecordReader对数据进行转换和提取:
RecordReader recordReader = new TransformProcessRecordReader(recordReader,transformProcess);

你可以在 github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data_Extraction_Transform_and_Loading/sourceCode/cookbook-app/src/main/java/com/javadeeplearningcookbook/app/recordreaderexamples/TransformProcessRecordReaderExample.java 找到 TransformProcessRecordReader 的示例。这个示例的数据集可以在 github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data_Extraction_Transform_and_Loading/transform-data.csv 找到。

  1. 使用 SequenceRecordReaderCodecRecordReader 提取序列数据:
RecordReader codecReader = new CodecRecordReader();
 codecReader.initialize(conf,split);

你可以在 github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data%20Extraction%2C%20Transform%20and%20Loading/sourceCode/cookbook-app/src/main/java/com/javadeeplearningcookbook/app/recordreaderexamples/CodecReaderExample.java 找到 CodecRecordReader 的示例。

下面的代码展示了如何使用 RegexSequenceRecordReader

RecordReader recordReader = new RegexSequenceRecordReader((\d{2}/\d{2}/\d{2}) (\d{2}:\d{2}:\d{2}) ([A-Z]) (.*)",skipNumLines);
 recordReader.initialize(new NumberedFileInputSplit(path/log%d.txt));

你可以在 github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data_Extraction_Transform_and_Loading/sourceCode/cookbook-app/src/main/java/com/javadeeplearningcookbook/app/recordreaderexamples/RegexSequenceRecordReaderExample.java 找到 RegexSequenceRecordReader 的示例。

这个数据集可以在 github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data_Extraction_Transform_and_Loading/logdata.zip 找到。

下面的代码展示了如何使用 CSVSequenceRecordReader

CSVSequenceRecordReader seqReader = new CSVSequenceRecordReader(skipNumLines, delimiter);
 seqReader.initialize(new FileSplit(file));

你可以在github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data%20Extraction%2C%20Transform%20and%20Loading/sourceCode/cookbook-app/src/main/java/com/javadeeplearningcookbook/app/recordreaderexamples/SequenceRecordReaderExample.java找到CSVSequenceRecordReader的示例。

该数据集可以在github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data_Extraction_Transform_and_Loading/dataset.zip找到。

  1. 使用JacksonLineRecordReader`:`提取 JSON/XML/YAML 数据。
RecordReader recordReader = new JacksonLineRecordReader(fieldSelection, new ObjectMapper(new JsonFactory()));
 recordReader.initialize(new FileSplit(new File("json_file.txt")));

你可以在github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data_Extraction_Transform_and_Loading/sourceCode/cookbook-app/src/main/java/com/javadeeplearningcookbook/app/recordreaderexamples/JacksonLineRecordReaderExample.java找到JacksonLineRecordReader的示例。

该数据集可以在github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data_Extraction_Transform_and_Loading/irisdata.txt找到。

它是如何工作的……

数据可能分布在多个文件、子目录或多个集群中。由于大小等各种限制,我们需要一种机制来以不同的方式提取和处理数据。在分布式环境中,大量数据可能作为块存储在多个集群中。DataVec 为此使用InputSplit

在第 1 步中,我们查看了FileSplit,它是一个InputSplit的实现,用于将根目录拆分为多个文件。FileSplit会递归地查找指定目录位置中的文件。你还可以传递一个字符串数组作为参数,用来表示允许的扩展名:

  • 示例输入:带有文件的目录位置:

  • 示例输出:应用过滤器后的 URI 列表:

在示例输出中,我们删除了所有非.jpeg格式的文件路径。如果您希望从 URI 列表中提取数据,就像我们在第 2 步中所做的,CollectionInputSplit会非常有用。在第 2 步中,temp目录中包含了一组文件,我们使用CollectionInputSplit从这些文件中生成了一个 URI 列表。虽然FileSplit专门用于将目录拆分为文件(即 URI 列表),CollectionInputSplit是一个简单的InputSplit实现,用于处理 URI 输入集合。如果我们已经有了一个待处理的 URI 列表,那么可以直接使用CollectionInputSplit而不是FileSplit

  • 示例输入:一个包含文件的目录位置。参考以下截图(包含图像文件的目录作为输入):

  • 示例输出:一组 URI 列表。参考前面提到的由CollectionInputSplit生成的 URI 列表。

在第 3 步中,NumberedFileInputSplit根据指定的编号格式生成 URI。

请注意,我们需要传递一个合适的正则表达式模式,以生成顺序格式的文件名。否则,将会抛出运行时错误。正则表达式使我们能够接受各种编号格式的输入。NumberedFileInputSplit将生成一个 URI 列表,您可以将其传递到下一级以提取和处理数据。我们在文件名末尾添加了%d正则表达式,以指定文件名末尾存在编号。

  • 示例输入:一个目录位置,里面有以编号命名格式的文件,例如,file1.txtfile2.txtfile3.txt

  • 示例输出:一组 URI 列表:

如果您需要将输入 URI 映射到不同的输出 URI,那么您需要TransformSplit。我们在第 4 步中使用它来规范化/转换数据 URI 为所需的格式。如果特征和标签存储在不同位置,它将特别有用。当执行第 4 步时,URI 中的"."字符串将被去除,从而生成以下 URI:

  • 示例输入:一组 URI 集合,类似我们在CollectionInputSplit中看到的。然而,TransformSplit可以接受有错误的 URI:

  • 示例输出:格式化后的 URI 列表:

执行第 5 步后,URI 中的-in.csv子串将被替换为-out.csv

CSVRecordReader是一个简单的 CSV 记录读取器,用于流式处理 CSV 数据。我们可以基于分隔符来形成数据流对象,并指定其他各种参数,例如跳过开头若干行。在第 6 步中,我们使用了CSVRecordReader来完成这一操作。

对于 CSVRecordReader 示例,使用本章 GitHub 仓库中包含的 titanic.csv 文件。你需要在代码中更新目录路径才能使用该文件。

ImageRecordReader 是一个用于流式传输图像数据的图像记录读取器。

在第 7 步中,我们从本地文件系统中读取图像。然后,我们根据给定的高度、宽度和通道进行缩放和转换。我们还可以指定要标记的图像数据的标签。为了指定图像集的标签,在根目录下创建一个单独的子目录,每个子目录代表一个标签。

在第 7 步中,ImageRecordReader 构造函数中的前两个参数代表图像要缩放到的高度和宽度。我们通常为表示 R、G 和 B 的通道给定值 3。parentPathLabelGenerator 将定义如何标记图像中的标签。trainData 是我们需要的 inputSplit,以便指定要加载的记录范围,而 transform 是在加载图像时要应用的图像转换。

对于 ImageRecordReader 示例,你可以从 ImageNet 下载一些示例图像。每个图像类别将由一个子目录表示。例如,你可以下载狗的图像并将它们放在名为 "dog" 的子目录下。你需要提供包含所有可能类别的父目录路径。

ImageNet 网站可以在 www.image-net.org/ 上找到。

TransformProcessRecordReader 在用于模式转换过程中时需要一些解释。TransformProcessRecordReader 是将模式转换应用于记录读取器后的最终产品。这将确保在将数据传入训练之前,已应用定义的转换过程。

在第 8 步中,transformProcess 定义了要应用于给定数据集的一系列转换。这可以是去除不需要的特征、特征数据类型转换等。目的是使数据适合神经网络进行进一步处理。在本章接下来的配方中,你将学习如何创建转换过程。

对于 TransformProcessRecordReader 示例,使用本章 GitHub 仓库中包含的 transform-data.csv 文件。你需要在代码中更新文件路径才能使用该文件。

在第 9 步中,我们查看了 SequenceRecordReader 的一些实现。如果我们有一系列记录需要处理,就使用这个记录读取器。这个记录读取器可以在本地以及分布式环境中使用(例如 Spark)。

对于 SequenceRecordReader 示例,你需要从本章的 GitHub 仓库中提取 dataset.zip 文件。提取后,你将看到两个子目录:featureslabels。在每个目录中都有一系列文件。你需要在代码中提供这两个目录的绝对路径。

CodecRecordReader 是一个处理多媒体数据集的记录读取器,可以用于以下目的:

  • H.264(AVC)主配置文件解码器

  • MP3 解码器/编码器

  • Apple ProRes 解码器和编码器

  • H264 基线配置文件编码器

  • Matroska (MKV) 解复用器和复用器

  • MP4(ISO BMF,QuickTime)解复用器/复用器和工具

  • MPEG 1/2 解码器

  • MPEG PS/TS 解复用器

  • Java 播放器小程序解析

  • VP8 编码器

  • MXF 解复用器

CodecRecordReader 使用 jcodec 作为底层媒体解析器。

对于 CodecRecordReader 示例,你需要在代码中提供一个短视频文件的目录位置。这个视频文件将作为 CodecRecordReader 示例的输入。

RegexSequenceRecordReader 会将整个文件视为一个单独的序列,并逐行读取。然后,它将使用指定的正则表达式拆分每一行。我们可以将 RegexSequenceRecordReaderNumberedFileInputSplit 结合使用来读取文件序列。在第 9 步中,我们使用 RegexSequenceRecordReader 来读取随时间步长记录的事务日志(时间序列数据)。在我们的数据集(logdata.zip)中,事务日志是无监督数据,没有特征或标签的说明。

对于 RegexSequenceRecordReader 示例,你需要从本章的 GitHub 仓库中提取 logdata.zip 文件。提取后,你将看到一系列带编号的事务日志文件。你需要在代码中提供提取目录的绝对路径。

CSVSequenceRecordReader 以 CSV 格式读取数据序列。每个序列代表一个单独的 CSV 文件。每一行代表一个时间步。

在第 10 步中,JacksonLineRecordReader 将逐行读取 JSON/XML/YAML 数据。它期望每行是有效的 JSON 条目,并且行末没有分隔符。这符合 Hadoop 的惯例,确保分割操作在集群环境中正常工作。如果记录跨越多行,分割可能无法按预期工作,并可能导致计算错误。与 JacksonRecordReader 不同,JacksonLineRecordReader 不会自动创建标签,且需要在训练时指定配置。

对于 JacksonLineRecordReader 示例,你需要提供 irisdata.txt 文件的目录位置,该文件位于本章的 GitHub 仓库中。在 irisdata.txt 文件中,每一行代表一个 JSON 对象。

还有更多...

JacksonRecordReader 是一个使用 Jackson API 的记录读取器。与 JacksonLineRecordReader 类似,它也支持 JSON、XML 和 YAML 格式。对于 JacksonRecordReader,用户需要提供一个要从 JSON/XML/YAML 文件中读取的字段列表。这可能看起来很复杂,但它允许我们在以下条件下解析文件:

  • JSON/XML/YAML 数据没有一致的模式。可以使用 FieldSelection 对象提供输出字段的顺序。

  • 有些文件中缺少某些字段,但可以通过FieldSelection对象提供。

JacksonRecordReader也可以与PathLabelGenerator一起使用,根据文件路径附加标签。

执行模式转换

数据转换是一个重要的数据标准化过程。可能会出现坏数据,例如重复项、缺失值、非数字特征等。我们需要通过应用模式转换对其进行标准化,以便数据可以在神经网络中处理。神经网络只能处理数值特征。在这个过程中,我们将展示模式创建的过程。

如何操作……

  1. 识别数据中的异常值:对于一个特征较少的小型数据集,我们可以通过手动检查来发现异常值/噪声。对于特征较多的数据集,我们可以执行主成分分析PCA),如以下代码所示:
INDArray factor = org.nd4j.linalg.dimensionalityreduction.PCA.pca_factor(inputFeatures, projectedDimension, normalize);
 INDArray reduced = inputFeatures.mmul(factor);
  1. 使用模式来定义数据结构:以下是一个客户流失数据集的基本模式示例。你可以从www.kaggle.com/barelydedicated/bank-customer-churn-modeling/downloads/bank-customer-churn-modeling.zip/1下载数据集:
  Schema schema = new Schema.Builder()
 .addColumnString("RowNumber")
 .addColumnInteger("CustomerId")
 .addColumnString("Surname")
 .addColumnInteger("CreditScore")
 .addColumnCategorical("Geography",  
  Arrays.asList("France","Germany","Spain"))
 .addColumnCategorical("Gender", Arrays.asList("Male","Female"))
 .addColumnsInteger("Age", "Tenure")
 .addColumnDouble("Balance")
 .addColumnsInteger("NumOfProducts","HasCrCard","IsActiveMember")
 .addColumnDouble("EstimatedSalary")
 .build();

它是如何工作的……

在我们开始创建模式之前,我们需要检查数据集中的所有特征。然后,我们需要清理所有噪声特征,例如名称,假设它们对生成的结果没有影响。如果某些特征不清楚,只需保留它们并将其包含在模式中。如果你删除了某个本来是有效信号的特征而不自知,那么你将降低神经网络的效率。这个过程就是在第 1 步中移除异常值并保留有效信号(有效特征)。主成分分析PCA)将是一个理想的选择,而且 ND4J 中已经实现了该方法。PCA类可以在特征数量较多的数据集中执行降维操作,帮助你减少特征数量,从而降低复杂性。减少特征意味着移除无关特征(异常值/噪声)。在第 1 步中,我们通过调用pca_factor()并传入以下参数生成了 PCA 因子矩阵:

  • inputFeatures:作为矩阵的输入特征

  • projectedDimension:从实际特征集中投影的特征数量(例如,从 1000 个特征中选取 100 个重要特征)

  • normalize:一个布尔变量(true/false),表示是否对特征进行标准化(零均值)

矩阵乘法通过调用 mmul() 方法来执行,最终结果是 reduced,这是我们在执行基于 PCA 因子的降维后使用的特征矩阵。请注意,你可能需要使用输入特征(这些特征是通过 PCA 因子生成的)进行多次训练,以理解信号。

在第 2 步中,我们使用了客户流失数据集(这是我们在下一章中使用的简单数据集)来演示 Schema 创建过程。模式中提到的数据类型是针对相应的特征或标签。例如,如果你想为整数特征添加模式定义,那么应该使用 addColumnInteger()。类似地,还有其他 Schema 方法可以用于管理其他数据类型。

类别变量可以使用 addColumnCategorical() 添加,正如我们在第 2 步中提到的那样。在这里,我们标记了类别变量并提供了可能的值。即使我们得到了一个被屏蔽的特征集,只要特征按编号格式排列(例如,column1column2 等),我们仍然可以构建它们的模式。

还有更多...

总的来说,构建数据集模式的步骤如下:

  • 充分了解你的数据。识别噪声和信号。

  • 捕获特征和标签。识别类别变量。

  • 识别可以应用一热编码的类别特征。

  • 注意缺失数据或不良数据。

  • 使用类型特定的方法,如 addColumnInteger()addColumnsInteger(),以整数为特征类型。对于其他数据类型,应用相应的 Builder 方法。

  • 使用 addColumnCategorical() 添加类别变量。

  • 调用 build() 方法来构建模式。

请注意,你不能跳过/忽略数据集中的任何特征,除非在模式中明确指定。你需要从数据集中移除异常特征,从剩余的特征中创建模式,然后继续进行转换过程进行进一步处理。或者,你也可以将所有特征放在一旁,将所有特征保留在模式中,并在转换过程中定义异常值。

在特征工程/数据分析方面,DataVec 提供了自己的分析引擎,用于对特征/目标变量进行数据分析。对于本地执行,我们可以使用 AnalyzeLocal 返回一个数据分析对象,该对象包含数据集每一列的信息。以下是如何从记录读取器对象创建数据分析对象的示例:

DataAnalysis analysis = AnalyzeLocal.analyze(mySchema, csvRecordReader);
 System.out.println(analysis);

你还可以通过调用 analyzeQuality() 来分析数据集中的缺失值,并检查它是否符合模式要求:

DataQualityAnalysis quality = AnalyzeLocal.analyzeQuality(mySchema, csvRecordReader);
 System.out.println(quality);

对于序列数据,你需要使用 analyzeQualitySequence() 而不是 analyzeQuality()。对于 Spark 上的数据分析,你可以使用 AnalyzeSpark 工具类来代替 AnalyzeLocal

构建转换过程

模式创建后的下一步是通过添加所有所需的转换来定义数据转换过程。我们可以使用TransformProcess管理有序的转换列表。在模式创建过程中,我们仅为数据定义了一个包含所有现有特征的结构,实际上并没有执行转换。让我们看看如何将数据集中的特征从非数值格式转换为数值格式。神经网络无法理解原始数据,除非它被映射到数值向量。在这个示例中,我们将根据给定的模式构建一个转换过程。

如何做...

  1. 将转换列表添加到TransformProcess中。考虑以下示例:
TransformProcess transformProcess = new TransformProcess.Builder(schema)
 .removeColumns("RowNumber","CustomerId","Surname")
 .categoricalToInteger("Gender")
 .categoricalToOneHot("Geography")
 .removeColumns("Geography[France]")
 .build();
  1. 使用TransformProcessRecordReader创建一个记录读取器,以提取并转换数据:
TransformProcessRecordReader transformProcessRecordReader = new TransformProcessRecordReader(recordReader,transformProcess);

它是如何工作的...

在第 1 步中,我们为数据集添加了所有需要的转换。TransformProcess定义了我们想要应用于数据集的所有转换的无序列表。我们通过调用removeColumns()移除了任何不必要的特征。在模式创建过程中,我们在Schema中标记了分类特征。现在,我们可以实际决定需要对特定的分类变量进行什么样的转换。通过调用categoricalToInteger(),分类变量可以转换为整数。如果调用categoricalToOneHot(),分类变量可以进行独热编码。请注意,模式必须在转换过程之前创建。我们需要模式来创建TransformProcess

在第 2 步中,我们借助TransformProcessRecordReader应用之前添加的转换。我们需要做的就是使用原始数据创建基础记录读取器对象,并将其传递给TransformProcessRecordReader,同时传递已定义的转换过程。

还有更多...

DataVec 允许我们在转换阶段做更多的事情。以下是TransformProcess中提供的其他一些重要转换功能:

  • addConstantColumn():在数据集中添加一个新列,列中的所有值都相同,并且与指定的值一致。此方法接受三个属性:新列的名称、新列的类型和该值。

  • appendStringColumnTransform():将一个字符串附加到指定的列中。此方法接受两个属性:要附加的列和要附加的字符串值。

  • conditionalCopyValueTransform():如果满足某个条件,则用另一个列中指定的值替换列中的值。此方法接受三个属性:要替换值的列、要参考值的列和要使用的条件。

  • conditionalReplaceValueTransform(): 如果条件满足,则用指定的值替换列中的值。该方法接受三个属性:要替换值的列、用于替换的值以及要使用的条件。

  • conditionalReplaceValueTransformWithDefault(): 如果条件满足,则用指定的值替换列中的值。否则,用另一个值填充该列。该方法接受四个属性:要替换值的列、条件满足时使用的值、条件不满足时使用的值,以及要使用的条件。

    我们可以使用 DataVec 中已编写的内置条件进行转换过程或数据清理过程。例如,我们可以使用NaNColumnCondition来替换NaN值,使用NullWritableColumnCondition来替换空值。

  • stringToTimeTransform(): 将字符串列转换为时间列。此方法针对在数据集中以字符串/对象形式保存的日期列。该方法接受三个属性:要使用的列名、要遵循的时间格式以及时区。

  • reorderColumns(): 使用新定义的顺序重新排列列。我们可以将列名按照指定顺序作为属性提供给此方法。

  • filter(): 根据指定的条件定义一个过滤过程。如果满足条件,则移除该示例或序列;否则,保留示例或序列。该方法只接受一个属性,即要应用的条件/过滤器。filter()方法在数据清理过程中非常有用。如果我们想要从指定列中移除NaN值,可以创建一个过滤器,如下所示:

Filter filter = new ConditionFilter(new NaNColumnCondition("columnName"));

如果我们想要从指定列中移除空值,可以创建一个过滤器,如下所示:

Filter filter =  new ConditionFilter(new NullWritableColumnCondition("columnName"));  

  • stringRemoveWhitespaceTransform(): 此方法从列值中移除空白字符。该方法只接受一个属性,即需要修剪空白的列。

  • integerMathOp(): 此方法用于对整数列执行数学运算,使用标量值。类似的方法适用于如doublelong等类型。该方法接受三个属性:要应用数学运算的整数列、数学运算本身以及用于数学运算的标量值。

TransformProcess不仅用于数据处理,还可以通过一定的余地来克服内存瓶颈。

请参考 DL4J API 文档,以了解更多强大的 DataVec 特性,帮助你进行数据分析任务。在 TransformProcess 中还支持其他有趣的操作,如 reduce()convertToString()。如果你是数据分析师,那么你应该知道,许多数据归一化策略可以在此阶段应用。你可以参考 deeplearning4j.org/docs/latest/datavec-normalization 了解更多关于可用归一化策略的信息。

序列化转换

DataVec 提供了将转换序列化的功能,使其能够在生产环境中移植。在本食谱中,我们将序列化转换过程。

如何操作...

  1. 将转换序列化为人类可读的格式。我们可以使用 TransformProcess 将其转换为 JSON,如下所示:
String serializedTransformString = transformProcess.toJson()

我们可以使用 TransformProcess 将其转换为 YAML,如下所示:

String serializedTransformString = transformProcess.toYaml()

你可以在 github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data_Extraction_Transform_and_Loading/sourceCode/cookbook-app/src/main/java/com/javadeeplearningcookbook/app/SerializationExample.java 中找到这个示例。

  1. 将 JSON 反序列化为 TransformProcess 如下所示:
TransformProcess tp = TransformProcess.fromJson(serializedTransformString)

你也可以像下面这样将 YAML 加载到 TransformProcess 中:

TransformProcess tp = TransformProcess.fromYaml(serializedTransformString)

它是如何工作的...

在步骤 1 中,toJson()TransformProcess 转换为 JSON 字符串,而 toYaml()TransformProcess 转换为 YAML 字符串。

这两种方法都可以用于 TransformProcess 的序列化。

在步骤 2 中,fromJson() 将 JSON 字符串反序列化为 TransformProcess,而 fromYaml() 将 YAML 字符串反序列化为 TransformProcess

serializedTransformString 是需要转换为 TransformProcess 的 JSON/YAML 字符串。

本食谱与应用程序迁移到不同平台时相关。

执行转换过程

在定义完转换过程之后,我们可以在受控的管道中执行它。它可以通过批处理执行,或者我们可以将任务分配到 Spark 集群中。如果数据集非常庞大,我们无法直接进行数据输入和执行。可以将任务分配到 Spark 集群上处理更大的数据集。你也可以执行常规的本地执行。在本食谱中,我们将讨论如何在本地以及远程执行转换过程。

如何操作...

  1. 将数据集加载到 RecordReader 中。在 CSVRecordReader 的情况下,加载 CSV 数据:
RecordReader reader = new CSVRecordReader(0,',');
 reader.initialize(new FileSplit(file)); 

  1. 使用 LocalTransformExecutor 在本地执行转换:
List<List<Writable>> transformed = LocalTransformExecutor.execute(recordReader, transformProcess)
  1. 使用SparkTransformExecutor在 Spark 中执行转换:
JavaRDD<List<Writable>> transformed = SparkTransformExecutor.execute(inputRdd, transformProcess)

它是如何工作的...

在步骤 1 中,我们将数据集加载到记录读取器对象中。为了演示,我们使用了CSVRecordReader

在步骤 2 中,只有当TransformProcess返回非顺序数据时,才能使用execute()方法。对于本地执行,假设你已经将数据集加载到一个RecordReader中。

关于LocalTransformExecutor的示例,请参考此源文件中的LocalExecuteExample.java

github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data_Extraction_Transform_and_Loading/sourceCode/cookbook-app/src/main/java/com/javadeeplearningcookbook/app/executorexamples/LocalExecuteExample.java

对于LocalTransformExecutor示例,你需要提供titanic.csv的文件路径。它位于本章的 GitHub 目录中。

在步骤 3 中,假设你已经将数据集加载到一个 JavaRDD 对象中,因为我们需要在 Spark 集群中执行 DataVec 转换过程。此外,只有当TransformProcess返回非顺序数据时,才可以使用execute()方法。

还有更多...

如果TransformProcess返回顺序数据,则应使用executeSequence()方法:

List<List<List<Writable>>> transformed = LocalTransformExecutor.executeSequence(sequenceRecordReader, transformProcess)

如果你需要根据joinCondition将两个记录读取器连接起来,则需要使用executeJoin()方法:

List<List<Writable>> transformed = LocalTransformExecutor.executeJoin(joinCondition, leftReader, rightReader) 

以下是本地/Spark 执行器方法的概述:

  • execute(): 这个方法将转换应用于记录读取器。LocalTransformExecutor将记录读取器作为输入,而SparkTransformExecutor则需要将输入数据加载到一个 JavaRDD 对象中。这不能用于顺序数据。

  • executeSequence(): 这个方法将转换应用于一个序列读取器。然而,转换过程应该从非顺序数据开始,然后将其转换为顺序数据。

  • executeJoin(): 这个方法用于根据joinCondition将两个不同的输入读取器连接起来。

  • executeSequenceToSeparate(): 这个方法将转换应用于一个序列读取器。然而,转换过程应该从顺序数据开始,并返回非顺序数据。

  • executeSequenceToSequence(): 这个方法将转换应用于一个序列读取器。然而,转换过程应该从顺序数据开始,并返回顺序数据。

为了提高网络效率对数据进行归一化处理

归一化使得神经网络的工作变得更容易。它帮助神经网络将所有特征平等对待,无论它们的数值范围如何。归一化的主要目标是将数据集中的数值安排在一个共同的尺度上,同时不会干扰数值范围之间的差异。并不是所有数据集都需要归一化策略,但如果它们的数值范围不同,那么对数据进行归一化是一个关键步骤。归一化对模型的稳定性/准确性有直接影响。ND4J 提供了多种预处理器来处理归一化。在这个示例中,我们将对数据进行归一化。

如何实现...

  1. 从数据中创建数据集迭代器。请参考以下 RecordReaderDataSetIterator 的演示:
DataSetIterator iterator = new RecordReaderDataSetIterator(recordReader,batchSize);

  1. 通过调用归一化实现的 fit() 方法来对数据集应用归一化。请参考以下 NormalizerStandardize 预处理器的演示:
DataNormalization dataNormalization = new NormalizerStandardize();
dataNormalization.fit(iterator);
  1. 调用 setPreprocessor() 设置数据集的预处理器:
iterator.setPreProcessor(dataNormalization);

其工作原理...

首先,你需要有一个迭代器来遍历和准备数据。在第 1 步中,我们使用记录读取器数据创建了数据集迭代器。迭代器的目的是更好地控制数据以及如何将其呈现给神经网络。

一旦确定了合适的归一化方法(在第 2 步中选择了 NormalizerStandardize),我们使用 fit() 方法将归一化应用到数据集。NormalizerStandardize 会将数据归一化,使得特征值具有零均值和标准差为 1。

本示例的代码可以在 github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/02_Data_Extraction_Transform_and_Loading/sourceCode/cookbook-app/src/main/java/com/javadeeplearningcookbook/app/NormalizationExample.java 找到。

  • 示例输入:包含特征变量的数据集迭代器(INDArray 格式)。迭代器是根据前面示例中提到的输入数据创建的。

  • 示例输出:请参考以下快照,查看对输入数据应用归一化后的标准化特征(INDArray 格式):

请注意,在应用归一化时,不能跳过第 3 步。如果不执行第 3 步,数据集将不会自动归一化。

还有更多内容...

预处理器通常具有默认的范围限制,从 01。如果你没有对数值范围较大的数据集应用归一化(当特征值过低或过高时),神经网络将倾向于偏向那些数值较高的特征值。因此,神经网络的准确性可能会大幅降低。

如果值分布在对称区间内,例如(01),则在训练过程中,所有特征值都被认为是等价的。因此,它也会影响神经网络的泛化能力。

以下是 ND4J 提供的预处理器:

  • NormalizerStandardize:一个用于数据集的预处理器,它将特征值归一化,使其具有均值和标准差为 1。

  • MultiNormalizerStandardize:一个用于多数据集的预处理器,它将特征值归一化,使其具有零均值和标准差为 1。

  • NormalizerMinMaxScaler:一个用于数据集的预处理器,它将特征值归一化,使其位于指定的最小值和最大值之间。默认范围是从 0 到 1。

  • MultiNormalizerMinMaxScaler:一个用于多数据集的预处理器,它将特征值归一化,使其位于指定的最小值和最大值之间。默认范围是从 0 到 1。

  • ImagePreProcessingScaler:一个用于图像的预处理器,具有最小和最大缩放。默认范围是(miRangemaxRange)–(01)。

  • VGG16ImagePreProcessor:一个专门针对 VGG16 网络架构的预处理器。它计算均值 RGB 值,并将其从训练集中的每个像素值中减去。

第三章:构建用于二分类的深度神经网络

在本章中,我们将使用标准的前馈网络架构开发一个深度神经网络DNN)。在我们逐步完成各个配方的过程中,我们会不断向应用程序添加组件和修改内容。如果你还没有阅读过,第一章,Java 深度学习简介,以及第二章,数据提取、转换和加载,请确保回顾这些内容。这有助于更好地理解本章中的配方。

我们将以客户保持预测为例,演示标准的前馈网络。这是一个至关重要的现实问题,每个企业都希望解决。企业希望在满意的客户身上投入更多资源,因为这些客户往往会成为长期客户。与此同时,预测流失客户有助于企业更加专注于做出能阻止客户流失的决策。

请记住,前馈网络实际上并不能提供有关决策结果的特征提示。它只会预测客户是否继续支持该组织。实际的特征信号是隐藏的,由神经网络来决定。如果你想记录那些控制预测结果的实际特征信号,可以使用自编码器来完成此任务。接下来,让我们来看一下如何为我们上述的用例构建一个前馈网络。

在本章中,我们将涵盖以下内容:

  • 从 CSV 输入提取数据

  • 从数据中删除异常值

  • 对数据应用变换

  • 为神经网络模型设计输入层

  • 为神经网络模型设计隐藏层

  • 为神经网络模型设计输出层

  • 对 CSV 数据进行神经网络模型的训练和评估

  • 部署神经网络模型并将其用作 API

技术要求

确保满足以下要求:

  • JDK 8 已安装并添加到 PATH。源代码需要 JDK 8 来执行。

  • Maven 已安装并添加到 PATH。我们稍后将使用 Maven 构建应用程序的 JAR 文件。

本章讨论的具体用例(客户保持预测)的实现可以在github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/03_Building_Deep_Neural_Networks_for_Binary_classification/sourceCode/cookbookapp/src/main/java/com/javadeeplearningcookbook/examples/CustomerRetentionPredictionExample.java找到。

克隆我们的 GitHub 仓库后,导航到 Java-Deep-Learning-Cookbook/03_Building_Deep_Neural_Networks_for_Binary_classification/sourceCode 目录。然后,通过导入 pom.xml 将 cookbookapp 项目作为 Maven 项目导入到你的 IDE 中。

数据集已包含在 cookbookapp 项目的 resources 目录中(Churn_Modelling.csv)。

然而,数据集可以在 www.kaggle.com/barelydedicated/bank-customer-churn-modeling/downloads/bank-customer-churn-modeling.zip/1 下载。

从 CSV 输入中提取数据

ETL(即提取、转换和加载的缩写)是网络训练之前的第一阶段。客户流失数据是 CSV 格式的。我们需要提取它,并将其放入记录读取对象中以进一步处理。在这个食谱中,我们从 CSV 文件中提取数据。

如何操作...

  1. 创建 CSVRecordReader 来保存客户流失数据:
RecordReader recordReader = new CSVRecordReader(1,',');
  1. 向 CSVRecordReader 添加数据:
File file = new File("Churn_Modelling.csv");
   recordReader.initialize(new FileSplit(file)); 

它是如何工作的...

来自数据集的 CSV 数据有 14 个特征。每一行代表一个客户/记录,如下图所示:

我们的数据集是一个包含 10,000 条客户记录的 CSV 文件,其中每条记录都有标签,指示客户是否离开了业务。第 0 至第 13 列表示输入特征。第 14^(列),Exited,表示标签或预测结果。我们正在处理一个监督学习模型,每个预测都被标记为 0 或 1,其中 0 表示满意的客户,1 表示已离开的不满意客户。数据集的第一行仅为特征标签,在处理数据时我们不需要这些标签。因此,我们在第 1 步中创建记录读取器实例时跳过了第一行。在第 1 步中,1 是需要跳过的行数。此外,我们提到了逗号分隔符(,),因为我们使用的是 CSV 文件。在第 2 步中,我们使用了 FileSplit 来指定客户流失数据集文件。我们还可以使用其他 InputSplit 实现来处理多个数据集文件,如 CollectionInputSplitNumberedFileInputSplit 等。

从数据中移除异常值

对于监督数据集,手动检查对于特征较少的数据集效果很好。随着特征数量的增加,手动检查变得不切实际。我们需要执行特征选择技术,如卡方检验、随机森林等,来处理大量特征。我们还可以使用自编码器来缩小相关特征的范围。记住,每个特征都应该对预测结果有公平的贡献。因此,我们需要从原始数据集中移除噪声特征,并保持其他所有内容,包括任何不确定的特征。在这个食谱中,我们将演示识别数据中异常值的步骤。

如何操作...

  1. 在训练神经网络之前,排除所有噪声特征。请在模式转换阶段移除噪声特征:
TransformProcess transformProcess = new TransformProcess.Builder(schema)
 .removeColumns("RowNumber","CustomerId","Surname")
 .build();    
  1. 使用 DataVec 分析 API 识别缺失值:
DataQualityAnalysis analysis = AnalyzeLocal.analyzeQuality(schema,recordReader);
 System.out.println(analysis); 
  1. 使用模式转换移除空值:
Condition condition = new NullWritableColumnCondition("columnName");
 TransformProcess transformProcess = new TransformProcess.Builder(schema)
   .conditionalReplaceValueTransform("columnName",new IntWritable(0),condition)
 .build();
  1. 使用模式转换移除 NaN 值:
Condition condition = new NaNColumnCondition("columnName");
 TransformProcess transformProcess = new TransformProcess.Builder(schema)
   .conditionalReplaceValueTransform("columnName",new IntWritable(0),condition)
 .build();

它是如何工作的...

如果您还记得我们的客户流失数据集,它包含 14 个特征:

执行第 1 步后,您将剩下 11 个有效特征。以下标记的特征对预测结果没有任何意义。例如,客户的名字并不会影响客户是否会离开公司。

在上述截图中,我们标出了不需要用于训练的特征。这些特征可以从数据集中删除,因为它们对结果没有影响。

在第 1 步中,我们使用removeColumns()方法在模式转换过程中标记了数据集中的噪声特征(RowNumberCustomeridSurname)以供移除。

本章使用的客户流失数据集只有 14 个特征。而且,这些特征标签是有意义的。因此,手动检查已经足够了。如果特征数量较多,您可能需要考虑使用PCA(即主成分分析),正如上一章所解释的那样。

在第 2 步中,我们使用了AnalyzeLocal工具类,通过调用analyzeQuality()方法来查找数据集中的缺失值。当您打印出DataQualityAnalysis对象中的信息时,应该看到以下结果:

如您在前面的截图中所见,每个特征都对其质量进行了分析(以无效/缺失数据为标准),并显示了计数,以便我们判断是否需要进一步标准化。由于所有特征看起来都正常,我们可以继续进行下一步。

处理缺失值有两种方法。要么删除整个记录,要么用一个值替代。在大多数情况下,我们不会删除记录,而是用一个值来表示缺失。在转换过程中,我们可以使用conditionalReplaceValueTransform()conditionalReplaceValueTransformWithDefault()来进行此操作。在第 3/4 步中,我们从数据集中移除了缺失或无效值。请注意,特征需要事先已知。我们不能检查所有特征以完成此操作。目前,DataVec 不支持此功能。您可以执行第 2 步来识别需要注意的特征。

还有更多内容...

我们在本章前面讨论了如何使用AnalyzeLocal工具类找出缺失值。我们还可以使用AnalyzeLocal执行扩展的数据分析。我们可以创建一个数据分析对象,包含数据集中每一列的信息。通过调用analyze()可以创建该对象,正如我们在前一章中讨论的那样。如果你尝试打印出数据分析对象的信息,它将如下所示:

它将计算数据集中所有特征的标准差、均值以及最小/最大值。同时,还会计算特征的数量,这对于识别特征中的缺失或无效值非常有帮助。

上面两个截图显示的是通过调用analyze()方法返回的数据分析结果。对于客户流失数据集,我们应该有 10,000 个特征总数,因为数据集中的记录总数为 10,000。

对数据应用转换

数据转换是一个至关重要的数据标准化过程,必须在将数据输入神经网络之前完成。我们需要将非数值特征转换为数值,并处理缺失值。在本食谱中,我们将执行模式转换,并在转换后创建数据集迭代器。

如何做...

  1. 将特征和标签添加到模式中:
Schema.Builder schemaBuilder = new Schema.Builder();
 schemaBuilder.addColumnString("RowNumber")
 schemaBuilder.addColumnInteger("CustomerId")
 schemaBuilder.addColumnString("Surname")
 schemaBuilder.addColumnInteger("CreditScore");
  1. 识别并将分类特征添加到模式中:
schemaBuilder.addColumnCategorical("Geography", Arrays.asList("France","Germany","Spain"))
 schemaBuilder.addColumnCategorical("Gender", Arrays.asList("Male","Female"));
  1. 从数据集中移除噪声特征:
Schema schema = schemaBuilder.build();
 TransformProcess.Builder transformProcessBuilder = new TransformProcess.Builder(schema);
 transformProcessBuilder.removeColumns("RowNumber","CustomerId","Surname");
  1. 转换分类变量:
transformProcessBuilder.categoricalToInteger("Gender");
  1. 通过调用categoricalToOneHot()应用独热编码:
transformProcessBuilder.categoricalToInteger("Gender")
 transformProcessBuilder.categoricalToOneHot("Geography");

  1. 通过调用removeColumns()移除Geography特征的相关依赖:
transformProcessBuilder.removeColumns("Geography[France]")

在这里,我们选择了France作为相关变量。

  1. 使用TransformProcessRecordReader提取数据并应用转换:
TransformProcess transformProcess = transformProcessBuilder.build();
 TransformProcessRecordReader transformProcessRecordReader = new TransformProcessRecordReader(recordReader,transformProcess);
  1. 创建数据集迭代器以进行训练/测试:
DataSetIterator dataSetIterator = new RecordReaderDataSetIterator.Builder(transformProcessRecordReader,batchSize) .classification(labelIndex,numClasses)
 .build();
  1. 规范化数据集:
DataNormalization dataNormalization = new NormalizerStandardize();
 dataNormalization.fit(dataSetIterator);
 dataSetIterator.setPreProcessor(dataNormalization);
  1. 将主数据集迭代器拆分为训练和测试迭代器:
DataSetIteratorSplitter dataSetIteratorSplitter = new DataSetIteratorSplitter(dataSetIterator,totalNoOfBatches,ratio);
  1. DataSetIteratorSplitter生成训练/测试迭代器:
DataSetIterator trainIterator = dataSetIteratorSplitter.getTrainIterator();
 DataSetIterator testIterator = dataSetIteratorSplitter.getTestIterator();

它是如何工作的...

所有特征和标签需要按照第 1 步和第 2 步中提到的添加到模式中。如果我们没有这么做,DataVec 将在数据提取/加载时抛出运行时错误。

在前面的截图中,运行时异常是由于 DataVec 引发的,原因是特征数量不匹配。如果我们为输入神经元提供的值与数据集中的实际特征数量不一致,就会发生这种情况。

从错误描述中可以看出,我们仅在模式中添加了 13 个特征,这导致在执行过程中发生了运行时错误。前三个特征,分别为RownumberCustomeridSurname,需要添加到模式中。请注意,尽管我们发现它们是噪声特征,但仍需要在模式中标记这些特征。你也可以手动从数据集中删除这些特征。如果你这么做,就不需要在模式中添加它们,因此也无需在转换阶段处理它们。

对于大型数据集,除非分析结果将其识别为噪声,否则可以将数据集中的所有特征添加到模式中。同样,我们需要将其他特征变量如AgeTenureBalanceNumOfProductsHasCrCardIsActiveMemberEstimatedSalaryExited添加到模式中。添加它们时,请注意变量类型。例如,BalanceEstimatedSalary具有浮动点精度,因此考虑将它们的数据类型设为 double,并使用addColumnDouble()将它们添加到模式中。

我们有两个特征,分别为 gender 和 geography,需要特别处理。这两个特征是非数字型的,它们的特征值表示类别值,而不是数据集中其他字段的数值。任何非数字特征都需要转换为数值,以便神经网络能够对特征值进行统计计算。在步骤 2 中,我们使用addColumnCategorical()将类别变量添加到模式中。我们需要在列表中指定类别值,addColumnCategorical()将基于指定的特征值标记整数值。例如,类别变量Gender中的MaleFemale值将分别被标记为01。在步骤 2 中,我们将类别变量的可能值添加到列表中。如果数据集中有其他未知类别值(与模式中提到的值不同),DataVec 将在执行过程中抛出错误。

在步骤 3 中,我们通过调用removeColumns()标记了需要在转换过程中移除的噪声特征。

在步骤 4 中,我们对Geography类别变量进行了独热编码。

Geography有三个分类值,因此在转换后它将采用 0、1 和 2 的值。转换非数值型值的理想方式是将它们转换为零(0)和一(1)的值。这将显著减轻神经网络的负担。此外,普通的整数编码仅在变量之间存在序数关系时适用。这里的风险在于,我们假设变量之间存在自然的顺序关系。这种假设可能会导致神经网络出现不可预测的行为。因此,我们在第 6 步中删除了相关变量。为了演示,我们在第 6 步中选择了France作为相关变量。但你可以从三个分类值中选择任何一个。这是为了消除任何影响神经网络性能和稳定性的相关性依赖。第 6 步后,Geography特征的最终模式将如下所示:

在第 8 步中,我们从记录读取器对象创建了数据集迭代器。以下是RecordReaderDataSetIterator构建方法的属性及其各自的作用:

  • labelIndex:在 CSV 数据中标签(结果)所在的索引位置。

  • numClasses:数据集中标签(结果)的数量。

  • batchSize:通过神经网络的数据块。如果你指定了批量大小为 10 且有 10,000 条记录,那么将会有 1,000 个批次,每个批次包含 10 条记录。

此外,我们这里有一个二分类问题,因此我们使用了classification()方法来指定标签索引和标签数量。

对于数据集中的某些特征,你可能会观察到特征值范围之间的巨大差异。有些特征的数值较小,而有些特征的数值非常大。这些大/小数值可能会被神经网络误解。神经网络可能会错误地为这些特征分配高/低优先级,从而导致错误或波动的预测。为了避免这种情况,我们必须在将数据集输入到神经网络之前对其进行归一化。因此,我们在第 9 步中执行了归一化操作。

在第 10 步中,我们使用DataSetIteratorSplitter将主数据集拆分用于训练或测试。

以下是DataSetIteratorSplitter的参数:

  • totalNoOfBatches:如果你指定了 10 的批量大小并且有 10,000 条记录,那么需要指定 1,000 作为批次的总数。

  • ratio:这是分割器分割迭代器集的比例。如果你指定 0.8,这意味着 80%的数据将用于训练,剩余的 20%将用于测试/评估。

为神经网络模型设计输入层

输入层设计需要理解数据如何流入系统。我们有 CSV 数据作为输入,需要检查特征来决定输入属性。层是神经网络架构的核心组件。在这个示例中,我们将为神经网络配置输入层。

准备工作

我们需要在设计输入层之前决定输入神经元的数量。它可以通过特征形状得出。例如,我们有 13 个输入特征(不包括标签)。但在应用变换后,我们的数据集总共有 11 个特征列。噪声特征被移除,类别变量在模式转换过程中被转化。因此,最终的转换数据将有 11 个输入特征。输入层的输出神经元没有特定要求。如果我们为输入层分配错误数量的输入神经元,可能会导致运行时错误:

DL4J 的错误堆栈几乎可以自解释可能的原因。它指明了需要修复的具体层(前面示例中的layer0)。

如何操作...

  1. 使用MultiLayerConfiguration定义神经网络配置:
MultiLayerConfiguration.Builder builder = new NeuralNetConfiguration.Builder().weightInit(WeightInit.RELU_UNIFORM)
 .updater(new Adam(0.015D))
 .list();

  1. 使用DenseLayer定义输入层配置:
builder.layer(new DenseLayer.Builder().nIn(incomingConnectionCount).nOut(outgoingConnectionCount).activation(Activation.RELU)
.build())
.build();

它是如何工作的...

我们通过调用layer()方法向网络中添加了层,如步骤 2 所述。输入层通过DenseLayer添加此外,我们需要为输入层添加激活函数。我们通过调用activation()方法指定激活函数。我们在第一章中讨论了激活函数,《Java 深度学习简介》。你可以使用 DL4J 中可用的激活函数之一来设置activation()方法。最常用的激活函数是RELU。以下是其他方法在层设计中的作用:

  • nIn():这指的是该层的输入数量。对于输入层,它就是输入特征的数量。

  • nOut():这指的是神经网络中到下一个全连接层的输出数量。

为神经网络模型设计隐藏层

隐藏层是神经网络的核心。实际的决策过程发生在那里。隐藏层的设计是基于达到某个层次,超过这个层次,神经网络就无法再优化的水平。这个水平可以定义为产生最佳结果的最优隐藏层数量。

隐藏层是神经网络将输入转化为输出层能够使用并进行预测的不同格式的地方。在这个示例中,我们将为神经网络设计隐藏层。

如何操作...

  1. 确定输入/输出连接。设置如下:
incoming neurons = outgoing neurons from preceding layer.
 outgoing neurons = incoming neurons for the next hidden layer.
  1. 使用DenseLayer配置隐藏层:
builder.layer(new DenseLayer.Builder().nIn(incomingConnectionCount).nOut(outgoingConnectionCount).activation(Activation.RELU).build());

它是如何工作的...

第一步,如果神经网络只有一个隐藏层,那么隐藏层中的神经元(输入)的数量应该与前一层的输出连接数相同。如果你有多个隐藏层,你还需要确认前一层隐藏层的这一点。

在确保输入层的神经元数量与前一层的输出神经元数量相同后,你可以使用 DenseLayer 创建隐藏层。在第二步中,我们使用 DenseLayer 为输入层创建了隐藏层。实际上,我们需要多次评估模型,以了解网络的表现。没有一种常规的层配置适用于所有模型。同时,RELU 是隐藏层的首选激活函数,因为它具有非线性特性。

为神经网络模型设计输出层

输出层设计需要理解期望的输出。我们的输入是 CSV 数据,输出层则依赖于数据集中的标签数量。输出层是根据隐藏层的学习过程形成实际预测的地方。

在这个方案中,我们将为神经网络设计输出层。

如何操作……

  1. 确定输入/输出连接。设置以下内容:
incoming neurons = outgoing neurons from preceding hidden layer.
 outgoing neurons = number of labels

  1. 配置神经网络的输出层:
builder.layer(new OutputLayer.Builder(new LossMCXENT(weightsArray)).nIn(incomingConnectionCount).nOut(labelCount).activation(Activation.SOFTMAX).build())

它是如何工作的……

第一步,我们需要确保前一层的 nOut() 与输出层的 nIn() 拥有相同数量的神经元。

所以, incomingConnectionCount 应该与前一层的 outgoingConnectionCount 相同。

我们在第一章《Java 中的深度学习介绍》中讨论过 SOFTMAX 激活函数。我们的使用案例(客户流失)是二分类模型的一个例子。我们希望得到一个概率性结果,也就是客户被标记为开心不开心的概率,其中 0 代表开心的客户,1 代表不开心的客户。这个概率将被评估,神经网络将在训练过程中自我训练。

输出层的适当激活函数是SOFTMAX。这是因为我们需要计算标签发生的概率,而这些概率的总和应该为 1。SOFTMAX与对数损失函数一起,对于分类模型能够产生良好的结果。引入weightsArray是为了在数据不平衡的情况下强制优先选择某个标签。在步骤 2 中,输出层是通过OutputLayer类创建的。唯一的区别是,OutputLayer需要一个误差函数来计算预测时的错误率。在我们的例子中,我们使用了LossMCXENT,它是一个多类交叉熵误差函数。我们的客户流失示例遵循二分类模型;然而,由于我们的示例中有两个类(标签),因此仍然可以使用此误差函数。在步骤 2 中,labelCount将为 2。

训练并评估 CSV 数据的神经网络模型

在训练过程中,神经网络学习执行预期任务。对于每次迭代/周期,神经网络都会评估其训练知识。因此,它会通过更新的梯度值重新迭代各个层,以最小化输出层产生的错误。此外,请注意,标签(01)在数据集中并不是均匀分布的。因此,我们可能需要考虑为在数据集中出现较少的标签添加权重。在实际的训练会话开始之前,强烈建议这样做。在这个例子中,我们将训练神经网络并评估结果模型。

如何操作……

  1. 创建一个数组为较少的标签分配权重:
INDArray weightsArray = Nd4j.create(new double[]{0.35, 0.65});
  1. 修改OutPutLayer以均衡数据集中的标签:
new OutputLayer.Builder(new LossMCXENT(weightsArray)).nIn(incomingConnectionCount).nOut(labelCount).activation(Activation.SOFTMAX))
.build();
  1. 初始化神经网络并添加训练监听器:
MultiLayerConfiguration configuration = builder.build();
   MultiLayerNetwork multiLayerNetwork = new MultiLayerNetwork(configuration);
 multiLayerNetwork.init();
 multiLayerNetwork.setListeners(new ScoreIterationListener(iterationCount));
  1. 添加 DL4J UI Maven 依赖项以分析训练过程:
<dependency>
 <groupId>org.deeplearning4j</groupId>
 <artifactId>deeplearning4j-ui_2.10</artifactId>
 <version>1.0.0-beta3</version>
 </dependency>
  1. 启动 UI 服务器并添加临时存储来存储模型信息:
UIServer uiServer = UIServer.getInstance();
 StatsStorage statsStorage = new InMemoryStatsStorage();

FileStatsStorage替换InMemoryStatsStorage(以应对内存限制):

multiLayerNetwork.setListeners(new ScoreIterationListener(100),
 new StatsListener(statsStorage));
  1. 为 UI 服务器分配临时存储空间:
uiServer.attach(statsStorage);
  1. 通过调用fit()训练神经网络:
multiLayerNetwork.fit(dataSetIteratorSplitter.getTrainIterator(),100);
  1. 通过调用evaluate()来评估模型:
Evaluation evaluation = multiLayerNetwork.evaluate(dataSetIteratorSplitter.getTestIterator(),Arrays.asList("0","1"));
 System.out.println(evaluation.stats()); //printing the evaluation metrics

它是如何工作的……

当神经网络提高其泛化能力时,它的效率会增加。神经网络不应该仅仅记住特定标签的决策过程。如果它这么做了,我们的结果将会有偏差并且是错误的。因此,最好使用标签均匀分布的数据集。如果标签不是均匀分布的,那么在计算错误率时我们可能需要调整一些东西。为此,我们在步骤 1 中引入了weightsArray,并在步骤 2 中将其添加到OutputLayer

对于 weightsArray = {0.35, 0.65},网络更优先考虑 1(客户不满意)的结果。如本章前面讨论的,Exited 列代表标签。如果我们观察数据集,很明显标签为 0(客户满意)的结果比 1 的记录更多。因此,我们需要给 1 分配额外的优先级,以便平衡数据集。否则,神经网络可能会过拟合,并且会偏向 1 标签。

在步骤 3 中,我们添加了 ScoreIterationListener 来记录训练过程的日志。请注意,iterationCount 是记录网络得分的迭代次数。记住,iterationCount 不是周期。我们说一个周期已经完成,当整个数据集已经通过神经网络前后传递一次(反向传播)。

在步骤 8 中,我们使用 dataSetIteratorSplitter 获取训练数据集的迭代器,并在其上训练我们的模型。如果你正确配置了日志记录器,你应该能够看到训练实例正在按以下方式进展:

截图中的得分并不是成功率,而是通过误差函数为每次迭代计算的误差率。

我们在步骤 4、5 和 6 中配置了 DL4J 用户界面 (UI)。DL4J 提供了一个 UI,可以在浏览器中可视化当前网络状态和训练进度(实时监控)。这将有助于进一步调优神经网络的训练。StatsListener 负责在训练开始时触发 UI 监控。UI 服务器的端口号为 9000。在训练进行时,可以访问 localhost:9000 以查看内容。我们应该能够看到如下内容:

我们可以参考概览部分中看到的第一张图来进行模型得分分析。图表中的 x 轴表示迭代次数y 轴表示模型得分

我们还可以进一步扩展研究,查看训练过程中激活值梯度更新参数的表现,可以通过检查图表中绘制的参数值来完成:

图表中的 x 轴都表示迭代次数,y 轴在参数更新图表中表示参数更新比率,而在激活/梯度图表中则表示标准差。

也可以进行逐层分析。我们只需点击左侧边栏中的模型标签,并选择所需的层进行进一步分析:

要分析内存消耗和 JVM 使用情况,我们可以在左侧边栏导航到系统标签:

我们也可以在同一位置详细查看硬件/软件的指标:

这对于基准测试也非常有用。正如我们所见,神经网络的内存消耗被明确标出,JVM/堆外内存消耗也在 UI 中提到,以分析基准测试的执行情况。

第 8 步后,评估结果将在控制台上显示:

在上述截图中,控制台显示了评估模型的各种评估指标。我们不能在所有情况下仅依赖某一特定指标,因此,评估模型时最好考虑多个指标。

我们的模型目前的准确率为 85.75%。我们有四个不同的性能指标,分别是准确率、精确率、召回率和 F1 得分。正如你在前面的截图中看到的,召回率指标不太理想,这意味着我们的模型仍然有假阴性案例。F1 得分在这里也很重要,因为我们的数据集具有不均衡的输出类别比例。我们不会详细讨论这些指标,因为它们超出了本书的范围。只要记住,所有这些指标都很重要,不能仅仅依赖准确率。当然,评估的权衡取决于具体问题。当前的代码已经经过优化,因此你会发现评估指标的准确率几乎稳定。对于一个训练良好的网络模型,这些性能指标的值将接近1

检查我们评估指标的稳定性非常重要。如果我们注意到对于未见数据评估指标不稳定,那么我们需要重新考虑网络配置的变化。

输出层的激活函数对输出的稳定性有影响。因此,充分理解输出要求肯定会在选择合适的输出函数(损失函数)时节省大量时间。我们需要确保神经网络具有稳定的预测能力。

还有更多内容...

学习率是决定神经网络效率的因素之一。高学习率会使输出偏离实际值,而低学习率则会导致由于收敛慢而学习过程缓慢。神经网络的效率还取决于我们在每一层中分配给神经元的权重。因此,在训练的早期阶段,权重的均匀分布可能会有所帮助。

最常用的方法是向各层引入丢弃法(dropout)。这迫使神经网络在训练过程中忽略一些神经元。这将有效防止神经网络记住预测过程。那么,如何判断一个网络是否记住了结果呢?其实,我们只需要将网络暴露于新数据。如果准确率指标变差,那么说明你遇到了过拟合问题。

提高神经网络效率(从而减少过拟合)另一种可能性是尝试在网络层中使用 L1/L2 正则化。当我们向网络层添加 L1/L2 正则化时,它会在误差函数中增加一个额外的惩罚项。L1 正则化通过惩罚神经元权重的绝对值之和,而 L2 正则化通过惩罚权重的平方和来实现。当输出变量是所有输入特征的函数时,L2 正则化能给出更好的预测。然而,当数据集存在离群点,并且并非所有特征都对预测输出变量有贡献时,L1 正则化更为优先。在大多数情况下,过拟合的主要原因是记忆化问题。此外,如果我们丢弃了过多的神经元,最终会导致欠拟合。这意味着我们丢失了比必要的更多有用数据。

请注意,权衡取舍可能会根据不同问题类型有所不同。仅靠准确度并不能确保每次都能得到良好的模型表现。如果我们不能承受假阳性预测的成本(例如垃圾邮件检测),则应评估精确度。如果我们不能承受假阴性预测的成本(例如欺诈交易检测),则应评估召回率。如果数据集中的类别分布不均,F1 分数是最优的。ROC 曲线适用于每个输出类别的观测值数量大致相等的情况。

一旦评估稳定,我们可以检查如何优化神经网络的效率。有多种方法可以选择。我们可以进行几次训练会话,尝试找出最优的隐藏层数量、训练轮数、丢弃率和激活函数。

以下截图指向了可能影响神经网络效率的各种超参数:

请注意,dropOut(0.9)意味着我们在训练过程中忽略 10%的神经元。

截图中的其他属性/方法如下:

  • weightInit():用于指定如何为每一层的神经元分配权重。

  • updater():用于指定梯度更新器配置。Adam是一种梯度更新算法。

在第十二章,基准测试与神经网络优化中,我们将通过一个超参数优化的示例,自动为你找到最优参数。它仅通过一次程序执行,就代表我们进行了多次训练会话,以找到最优值。如果你对将基准测试应用到实际应用中感兴趣,可以参考第十二章,基准测试与神经网络优化

部署神经网络模型并将其用作 API

在训练实例完成后,我们应该能够持久化模型,并且将其作为 API 重用。通过 API 访问客户流失模型,将允许外部应用程序预测客户保持率。我们将使用 Spring Boot 和 Thymeleaf 进行 UI 演示,并将应用程序本地部署和运行。在本教程中,我们将为客户流失示例创建一个 API。

准备工作

作为创建 API 的前提条件,您需要运行主要的示例源代码:

github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/03_Building_Deep_Neural_Networks_for_Binary_classification/sourceCode/cookbookapp/src/main/java/com/javadeeplearningcookbook/examples/CustomerRetentionPredictionExample.java

DL4J 有一个名为ModelSerializer的工具类,用于保存和恢复模型。我们已经使用ModelSerializer将模型持久化到磁盘,具体如下:

File file = new File("model.zip");
 ModelSerializer.writeModel(multiLayerNetwork,file,true);
 ModelSerializer.addNormalizerToModel(file,dataNormalization);

欲了解更多信息,请参阅:

github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/03_Building_Deep_Neural_Networks_for_Binary_classification/sourceCode/cookbookapp/src/main/java/com/javadeeplearningcookbook/examples/CustomerRetentionPredictionExample.java#L124

另外,请注意,我们需要将归一化预处理器与模型一起持久化。然后,我们可以在运行时重新使用该归一化器来规范化用户输入。在前面提到的代码中,我们通过调用addNormalizerToModel()ModelSerializer持久化了归一化器。

您还需要注意addNormalizerToModel()方法的以下输入属性:

  • multiLayerNetwork:神经网络训练所使用的模型

  • dataNormalization:我们用于训练的归一化器

请参考以下示例来实现具体的 API:

github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/03_Building_Deep_Neural_Networks_for_Binary_classification/sourceCode/cookbookapp/src/main/java/com/javadeeplearningcookbook/api/CustomerRetentionPredictionApi.java

在我们的 API 示例中,我们恢复了模型文件(即之前持久化的模型),以生成预测。

如何操作...

  1. 创建一个方法,用于生成用户输入的架构:
private static Schema generateSchema(){
 Schema schema = new Schema.Builder()
 .addColumnString("RowNumber")
 .addColumnInteger("CustomerId")
 .addColumnString("Surname")
 .addColumnInteger("CreditScore")
 .addColumnCategorical("Geography", Arrays.asList("France","Germany","Spain"))
 .addColumnCategorical("Gender", Arrays.asList("Male","Female"))
 .addColumnsInteger("Age", "Tenure")
 .addColumnDouble("Balance")
 .addColumnsInteger("NumOfProducts","HasCrCard","IsActiveMember")
 .addColumnDouble("EstimatedSalary")
 .build();
 return schema;
 }
  1. 从模式中创建TransformProcess
private static RecordReader applyTransform(RecordReader recordReader, Schema schema){
 final TransformProcess transformProcess = new TransformProcess.Builder(schema)
 .removeColumns("RowNumber","CustomerId","Surname")
 .categoricalToInteger("Gender")
 .categoricalToOneHot("Geography")
 .removeColumns("Geography[France]")
 .build();
 final TransformProcessRecordReader transformProcessRecordReader = new TransformProcessRecordReader(recordReader,transformProcess);
 return transformProcessRecordReader;
}
  1. 将数据加载到记录读取器实例中:
private static RecordReader generateReader(File file) throws IOException, InterruptedException {
 final RecordReader recordReader = new CSVRecordReader(1,',');
 recordReader.initialize(new FileSplit(file));
 final RecordReader transformProcessRecordReader=applyTransform(recordReader,generateSchema());
  1. 使用ModelSerializer恢复模型:
File modelFile = new File(modelFilePath);
 MultiLayerNetwork network = ModelSerializer.restoreMultiLayerNetwork(modelFile);
 NormalizerStandardize normalizerStandardize = ModelSerializer.restoreNormalizerFromFile(modelFile);

  1. 创建一个迭代器来遍历整个输入记录集:
DataSetIterator dataSetIterator = new RecordReaderDataSetIterator.Builder(recordReader,1).build();
 normalizerStandardize.fit(dataSetIterator);
 dataSetIterator.setPreProcessor(normalizerStandardize); 
  1. 设计一个 API 函数来根据用户输入生成输出:
public static INDArray generateOutput(File inputFile, String modelFilePath) throws IOException, InterruptedException {
 File modelFile = new File(modelFilePath);
 MultiLayerNetwork network = ModelSerializer.restoreMultiLayerNetwork(modelFile);
   RecordReader recordReader = generateReader(inputFile);
 NormalizerStandardize normalizerStandardize = ModelSerializer.restoreNormalizerFromFile(modelFile);
 DataSetIterator dataSetIterator = new RecordReaderDataSetIterator.Builder(recordReader,1).build();
 normalizerStandardize.fit(dataSetIterator);
 dataSetIterator.setPreProcessor(normalizerStandardize);
 return network.output(dataSetIterator);
 }

更多示例,请见:github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/03_Building_Deep_Neural_Networks_for_Binary_classification/sourceCode/cookbookapp/src/main/java/com/javadeeplearningcookbook/api/CustomerRetentionPredictionApi.java

  1. 通过运行 Maven 命令构建一个包含你 DL4J API 项目的阴影 JAR:
mvn clean install
  1. 运行源目录中包含的 Spring Boot 项目。将 Maven 项目导入到你的 IDE 中:github.com/PacktPublishing/Java-Deep-Learning-Cookbook/tree/master/03_Building_Deep_Neural_Networks_for_Binary_classification/sourceCode/spring-dl4j

在运行配置中添加以下 VM 选项:

-DmodelFilePath={PATH-TO-MODEL-FILE}

PATH-TO-MODEL-FILE是你存储实际模型文件的位置。它可以在本地磁盘或云端。

然后,运行SpringDl4jApplication.java文件:

  1. http://localhost:8080/上测试你的 Spring Boot 应用:

  1. 通过上传输入的 CSV 文件来验证功能。

使用一个示例 CSV 文件上传到 Web 应用:github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/03_Building_Deep_Neural_Networks_for_Binary_classification/sourceCode/cookbookapp/src/main/resources/test.csv

预测结果将如下所示:

它是如何工作的...

我们需要创建一个 API 来接收终端用户的输入并生成输出。终端用户将上传一个包含输入的 CSV 文件,API 则将预测结果返回给用户。

在第 1 步中,我们为输入数据添加了模式。用户输入应该遵循我们训练模型时的模式结构,唯一的区别是Exited标签没有添加,因为那是训练模型需要预测的任务。在第 2 步中,我们根据第 1 步创建的Schema创建了TransformProcess

在第 3 步中,我们使用第 2 步中的TransformProcess创建了一个记录读取器实例。这样可以从数据集中加载数据。

我们预计最终用户会上传批量输入以生成结果。因此,需要按照第 5 步创建一个迭代器,以遍历所有输入记录集。我们使用第 4 步中的预训练模型来设置迭代器的预处理器。另外,我们使用了batchSize值为1。如果你有更多的输入样本,可以指定一个合理的批量大小。

在第 6 步中,我们使用名为modelFilePath的文件路径来表示模型文件的位置。我们将其作为命令行参数从 Spring 应用程序中传递。这样,你可以配置你自己的自定义路径,以保存模型文件。在第 7 步之后,将创建一个带阴影的 JAR 文件,包含所有 DL4J 依赖项,并保存在本地 Maven 仓库中。你也可以在项目的目标仓库中查看该 JAR 文件。

客户留存 API 的依赖项已经添加到 Spring Boot 项目的pom.xml文件中,如下所示:

<dependency>
   <groupId>com.javadeeplearningcookbook.app</groupId>
   <artifactId>cookbookapp</artifactId>
   <version>1.0-SNAPSHOT</version>
 </dependency>

一旦按照第 7 步创建了带阴影的 JAR 文件,Spring Boot 项目将能够从本地仓库获取依赖项。因此,在导入 Spring Boot 项目之前,你需要先构建 API 项目。同时,确保像第 8 步中提到的那样,将模型文件路径添加为 VM 参数。

简而言之,运行用例所需的步骤如下:

  1. 导入并构建客户流失 API 项目:github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/03_Building_Deep_Neural_Networks_for_Binary_classification/sourceCode/cookbookapp/.

  2. 运行主示例以训练模型并保存模型文件:github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/03_Building_Deep_Neural_Networks_for_Binary_classification/sourceCode/cookbookapp/src/main/java/com/javadeeplearningcookbook/examples/CustomerRetentionPredictionExample.java.

  3. 构建客户流失 API 项目:github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/03_Building_Deep_Neural_Networks_for_Binary_classification/sourceCode/cookbookapp/.

  4. 通过运行此处的启动器来运行 Spring Boot 项目(使用之前提到的 VM 参数):github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/03_Building_Deep_Neural_Networks_for_Binary_classification/sourceCode/spring-dl4j/src/main/java/com/springdl4j/springdl4j/SpringDl4jApplication.java.

第四章:构建卷积神经网络

在本章中,我们将使用 DL4J 开发一个卷积神经网络CNN)进行图像分类示例。我们将在逐步推进配方的过程中,逐步开发应用程序的各个组件。本章假设你已经阅读了第一章,《Java 深度学习简介》以及第二章,《数据提取、转换与加载》,并且你已经按照第一章《Java 深度学习简介》中提到的内容在你的计算机上设置了 DL4J。现在,让我们开始讨论本章所需的具体更改。

出于演示目的,我们将对四种不同物种进行分类。CNN 将复杂的图像转换为可以用于预测的抽象格式。因此,CNN 将是解决此图像分类问题的最佳选择。

CNN 就像任何其他深度神经网络一样,抽象了决策过程,并为我们提供了一个将输入转化为输出的接口。唯一的区别是它们支持其他类型的层和不同的层次顺序。与文本或 CSV 等其他类型的输入不同,图像是复杂的。考虑到每个像素都是信息源,训练过程对于大量高分辨率图像将变得资源密集且耗时。

在本章中,我们将涵盖以下配方:

  • 从磁盘提取图像

  • 为训练数据创建图像变体

  • 图像预处理和输入层设计

  • 构建 CNN 的隐藏层

  • 构建用于输出分类的输出层

  • 训练图像并评估 CNN 输出

  • 为图像分类器创建 API 端点

技术要求

本章讨论的用例实现可以在这里找到:github.com/PacktPublishing/Java-Deep-Learning-Cookbook/tree/master/04_Building_Convolutional_Neural_Networks/sourceCode

克隆我们的 GitHub 仓库后,导航到以下目录:Java-Deep-Learning-Cookbook/04_Building_Convolutional_Neural_Networks/sourceCode。然后,通过导入pom.xml,将cookbookapp项目作为 Maven 项目导入。

你还会找到一个基本的 Spring 项目,spring-dl4j,也可以作为 Maven 项目导入。

本章将使用来自牛津的狗品种分类数据集。

主要数据集可以从以下链接下载:

www.kaggle.com/zippyz/cats-and-dogs-breeds-classification-oxford-dataset

要运行本章的源代码,请从以下链接下载数据集(仅限四个标签):

github.com/PacktPublishing/Java-Deep-Learning-Cookbook/raw/master/04_Building%20Convolutional%20Neural%20Networks/dataset.zip(可以在Java-Deep-Learning-Cookbook/04_Building Convolutional Neural Networks/目录中找到)。

解压缩数据集文件。图像保存在不同的目录中。每个目录代表一个标签/类别。为演示目的,我们使用了四个标签。但是,您可以尝试使用来自不同类别的更多图像以运行我们在 GitHub 上的示例。

请注意,我们的示例针对四个类别进行了优化。使用更多标签进行实验需要进一步的网络配置优化。

要在您的 CNN 中利用 OpenCV 库的能力,请添加以下 Maven 依赖项:

<dependency>
 <groupId>org.bytedeco.javacpp-presets</groupId>
 <artifactId>opencv-platform</artifactId>
 <version>4.0.1-1.4.4</version>
 </dependency>

我们将使用 Google Cloud SDK 在云中部署应用程序。有关详细说明,请参阅github.com/GoogleCloudPlatform/app-maven-plugin。有关 Gradle 的说明,请参阅github.com/GoogleCloudPlatform/app-gradle-plugin

从磁盘中提取图像

对于基于N个标签的分类,父目录中将创建N个子目录。提及父目录路径以进行图像提取。子目录名称将被视为标签。在本示例中,我们将使用 DataVec 从磁盘提取图像。

操作步骤...

  1. 使用FileSplit来定义加载到神经网络中的文件范围:
FileSplit fileSplit = new FileSplit(parentDir, NativeImageLoader.ALLOWED_FORMATS,new Random(42));
 int numLabels = fileSplit.getRootDir().listFiles(File::isDirectory).length;
  1. 使用ParentPathLabelGeneratorBalancedPathFilter来对标记数据集进行采样并将其分为训练/测试集:
ParentPathLabelGenerator parentPathLabelGenerator = new ParentPathLabelGenerator();
 BalancedPathFilter balancedPathFilter = new BalancedPathFilter(new Random(42),NativeImageLoader.ALLOWED_FORMATS,parentPathLabelGenerator);
 InputSplit[] inputSplits = fileSplit.sample(balancedPathFilter,trainSetRatio,testSetRatio);

工作原理...

在步骤 1 中,我们使用了FileSplit根据文件类型(PNG、JPEG、TIFF 等)来过滤图像。

我们还传入了一个基于单个种子的随机数生成器。此种子值是一个整数(我们的示例中为42)。FileSplit将能够利用随机种子以随机顺序生成文件路径列表。这将为概率决策引入更多随机性,从而提高模型的性能(准确性指标)。

如果您有一个包含未知数量标签的预制数据集,则计算numLabels至关重要。因此,我们使用了FileSplit来以编程方式计算它们:

int numLabels = fileSplit.getRootDir().listFiles(File::isDirectory).length; 

在步骤 2 中,我们使用了ParentPathLabelGenerator来根据目录路径为文件生成标签。同时,使用BalancedPathFilter来随机化数组中的路径顺序。随机化有助于克服过拟合问题。BalancedPathFilter还确保每个标签具有相同数量的路径,并帮助获得用于训练的最佳批次。

使用 testSetRatio20,数据集的 20% 将用作模型评估的测试集。在第 2 步后,inputSplits 中的数组元素将代表训练/测试数据集:

  • inputSplits[0] 将代表训练数据集。

  • inputSplits[1] 将代表测试数据集。

  • NativeImageLoader.ALLOWED_FORMATS 使用 JavaCV 加载图像。允许的图像格式有:.bmp.gif.jpg.jpeg.jp2.pbm.pgm.ppm.pnm.png.tif.tiff.exr.webp

  • BalancedPathFilter 随机化文件路径数组的顺序,并随机移除它们,使每个标签的路径数相同。它还会根据标签在输出中形成路径,以便获得易于优化的训练批次。因此,它不仅仅是随机抽样。

  • fileSplit.sample() 根据前述路径过滤器抽样文件路径。

它将进一步将结果拆分为一组 InputSplit 对象。每个对象将引用训练/测试集,其大小与前述权重成比例。

为训练数据创建图像变体

我们通过创建图像变体并在其基础上进一步训练我们的网络模型,以增强 CNN 的泛化能力。训练 CNN 时,使用尽可能多的图像变体是至关重要的,以提高准确性。我们基本上通过翻转或旋转图像来获得同一图像的更多样本。在本教程中,我们将使用 DL4J 中 ImageTransform 的具体实现来转换和创建图像样本。

如何做...

  1. 使用 FlipImageTransform 来水平或垂直翻转图像(随机或非随机):
ImageTransform flipTransform = new FlipImageTransform(new Random(seed));
  1. 使用 WarpImageTransform 来确定性或随机地扭曲图像的透视:
ImageTransform warpTransform = new WarpImageTransform(new Random(seed),delta);
  1. 使用 RotateImageTransform 以确定性或随机方式旋转图像:
ImageTransform rotateTransform = new RotateImageTransform(new Random(seed), angle);
  1. 使用 PipelineImageTransform 向管道中添加图像变换:
List<Pair<ImageTransform,Double>> pipeline = Arrays.asList(
 new Pair<>(flipTransform, flipImageTransformRatio),
 new Pair<>(warpTransform , warpImageTransformRatio)
 );
 ImageTransform transform = new PipelineImageTransform(pipeline);

它是如何工作的...

在第 1 步中,如果我们不需要随机翻转而是指定的翻转模式(确定性),那么我们可以进行如下操作:

int flipMode = 0;
ImageTransform flipTransform = new FlipImageTransform(flipMode);

flipMode 是确定性的翻转模式。

  • flipMode = 0: 绕 x 轴翻转

  • flipMode > 0: 绕 y 轴翻转

  • flipMode < 0: 绕两个轴翻转

在第 2 步中,我们传入了两个属性:Random(seed)deltadelta 是图像扭曲的幅度。查看以下图像示例,了解图像扭曲的演示:

(图像来源: https://commons.wikimedia.org/wiki/File:Image_warping_example.jpg

许可证:CC BY-SA 3.0

WarpImageTransform(new Random(seed), delta) 内部调用以下构造函数:

public WarpImageTransform(java.util.Random random,
 float dx1,
 float dy1,
 float dx2,
 float dy2,
 float dx3,
 float dy3,
 float dx4,
 float dy4

它将假设 dx1=dy1=dx2=dy2=dx3=dy3=dx4=dy4=delta

以下是参数描述:

  • dx1: 顶部左角的最大 x 轴扭曲量(像素)

  • dy1: 顶部左角的最大 y 轴扭曲量(像素)

  • dx2: 顶部右角的最大 x 轴扭曲量(像素)

  • dy2: 顶部右角的最大 y 轴扭曲量(像素)

  • dx3:右下角在x方向的最大变形(像素)

  • dy3:右下角在y方向的最大变形(像素)

  • dx4:左下角在x方向的最大变形(像素)

  • dy4:左下角在y方向的最大变形(像素)

在创建ImageRecordReader时,delta 的值将根据归一化的宽度/高度自动调整。这意味着给定的 delta 值将相对于创建ImageRecordReader时指定的归一化宽度/高度进行处理。假设我们在一个 100 x 100 像素的图像上,在* x * / * y 轴上进行 10 像素的变形。如果该图像被归一化为 30 x 30 大小,那么 x * / * y *轴上将进行 3 像素的变形。由于没有常量/最小/最大delta值能解决所有类型的图像分类问题,因此你需要尝试不同的delta值。

在步骤 3 中,我们使用了RotateImageTransform来执行旋转图像变换,通过在指定角度上旋转图像样本。

在步骤 4 中,我们通过PipelineImageTransform的帮助将多个图像变换添加到管道中,以便按顺序或随机地加载它们用于训练。我们创建了一个类型为List<Pair<ImageTransform,Double>>的管道。Pair中的Double值是管道中某个特定元素(ImageTransform)执行的概率

图像变换将帮助 CNN 更好地学习图像模式。在变换图像上进行训练将进一步避免过拟合的可能性。

还有更多...

WarpImageTransform在后台会调用 JavaCPP 方法warpPerspective(),并使用给定的属性interModeborderModeborderValue。JavaCPP 是一个 API,它解析本地 C/C++文件并生成 Java 接口作为包装器。我们之前在pom.xml中添加了 OpenCV 的 JavaCPP 依赖项。这将使我们能够利用 OpenCV 库进行图像变换。

图像预处理和输入层设计

归一化是 CNN 的一个关键预处理步骤,就像任何前馈神经网络一样。图像数据是复杂的。每个图像包含多个像素的信息。此外,每个像素都是一个信息源。我们需要归一化这些像素值,以便神经网络在训练时不会出现过拟合或欠拟合的问题。卷积/子采样层在设计 CNN 的输入层时也需要指定。在本方案中,我们将首先进行归一化,然后设计 CNN 的输入层。

如何操作...

  1. 创建ImagePreProcessingScaler进行图像归一化:
DataNormalization scaler = new ImagePreProcessingScaler(0,1);

  1. 创建神经网络配置并添加默认的超参数:
MultiLayerConfiguration.Builder builder = new NeuralNetConfiguration.Builder().weightInit(WeightInit.DISTRIBUTION)
 .dist(new NormalDistribution(0.0, 0.01))
 .activation(Activation.RELU)
 .updater(new Nesterovs(new StepSchedule(ScheduleType.ITERATION, 1e-2, 0.1, 100000), 0.9))
 .biasUpdater(new Nesterovs(new StepSchedule(ScheduleType.ITERATION, 2e-2, 0.1, 100000), 0.9))
 .gradientNormalization(GradientNormalization.RenormalizeL2PerLayer) // normalize to prevent vanishing or exploding gradients
 .l2(l2RegularizationParam)
 .list();
  1. 使用ConvolutionLayer为 CNN 创建卷积层:
builder.layer(new ConvolutionLayer.Builder(11,11)
 .nIn(channels)
 .nOut(96)
 .stride(1,1)
 .activation(Activation.RELU)
 .build());
  1. 使用SubsamplingLayer配置子采样层:
builder.layer(new SubsamplingLayer.Builder(PoolingType.MAX)
 .kernelSize(kernelSize,kernelSize)
 .build());
  1. 使用LocalResponseNormalization在层之间进行激活归一化:
 builder.layer(1, new LocalResponseNormalization.Builder().name("lrn1").build());

它是如何工作的...

在步骤 1 中,ImagePreProcessingScaler 将像素值标准化到指定的范围(0, 1)。我们将在创建数据迭代器后使用此标准化器。

在步骤 2 中,我们添加了超参数,如 L2 正则化系数、梯度归一化策略、梯度更新算法和全局激活函数(适用于所有层)。

在步骤 3 中,ConvolutionLayer 需要您指定核的维度(之前代码中的 11*11)。在 CNN 中,核充当特征检测器:

  • stride:指定在像素网格操作中每个样本之间的空间。

  • channels:输入神经元的数量。我们在这里提到颜色通道的数量(RGB:3)。

  • OutGoingConnectionCount:输出神经元的数量。

在步骤 4 中,SubsamplingLayer 是一个下采样层,用于减少要传输或存储的数据量,同时保持重要特征的完整。最大池化是最常用的采样方法。ConvolutionLayer 后面总是跟着 SubsamplingLayer

在 CNN 中,效率是一个具有挑战性的任务。它需要大量图像以及转换操作来进行更好的训练。在步骤 4 中,LocalResponseNormalization 提高了 CNN 的泛化能力。在执行 ReLU 激活之前,它会执行归一化操作。

我们将其作为一个独立的层,放置在卷积层和下采样层之间:

  • ConvolutionLayer 类似于前馈层,但用于在图像上执行二维卷积操作。

  • SubsamplingLayer 是 CNN 中池化/下采样所必需的。

  • ConvolutionLayerSubsamplingLayer 一起构成 CNN 的输入层,从图像中提取抽象特征,并将其传递到隐藏层以进行进一步处理。

构建 CNN 的隐藏层

CNN 的输入层生成抽象图像并将其传递给隐藏层。抽象图像特征从输入层传递到隐藏层。如果您的 CNN 中有多个隐藏层,那么每个层将有独特的责任来进行预测。例如,其中一个层可以检测图像中的亮暗,而随后的层可以借助前一层的特征来检测边缘/形状。接下来的层可以从前一隐藏层的边缘/特征中辨识出更复杂的物体或配方,以此类推。

在这个方案中,我们将为我们的图像分类问题设计隐藏层。

如何操作...

  1. 使用 DenseLayer 构建隐藏层:
new DenseLayer.Builder()
 .nOut(nOut)
 .dist(new NormalDistribution(0.001, 0.005))
 .activation(Activation.RELU)
 .build();
  1. 通过调用 layer() 添加 AddDenseLayer 到层结构中:
builder.layer(new DenseLayer.Builder()
 .nOut(500)
 .dist(new NormalDistribution(0.001, 0.005))
 .activation(Activation.RELU)
 .build());

它是如何工作的...

在步骤 1 中,隐藏层通过 DenseLayer 创建,前面是卷积/下采样层。

在步骤 2 中,注意我们没有提到隐藏层中输入神经元的数量,因为它与前一层(SubSamplingLayer)的输出神经元数量相同。

构建输出层进行输出分类

我们需要使用逻辑回归(SOFTMAX)进行图像分类,从而得到每个图像标签的发生概率。逻辑回归是一种预测分析算法,因此更适合用于预测问题。在本配方中,我们将设计图像分类问题的输出层。

如何实现...

  1. 使用 OutputLayer 设计输出层:
builder.layer(new OutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
 .nOut(numLabels)
 .activation(Activation.SOFTMAX)
 .build());
  1. 使用 setInputType() 设置输入类型:
builder.setInputType(InputType.convolutional(30,30,3));

它是如何工作的...

在第一步中,nOut() 预期的是我们在前面的配方中使用 FileSplit 计算出的图像标签数量。

在第二步中,我们使用 setInputType() 设置了卷积输入类型。这将触发输入神经元的计算/设置,并添加预处理器(LocalResponseNormalization)以处理从卷积/子采样层到全连接层的数据流。

InputType 类用于跟踪和定义激活类型。这对自动添加层间预处理器以及自动设置 nIn(输入神经元数量)值非常有用。也正是这样,我们在配置模型时跳过了 nIn 值的指定。卷积输入类型的形状是四维的 [miniBatchSize, channels, height, width]

训练图像并评估 CNN 输出

我们已经设置了层的配置。现在,我们需要训练 CNN 使其适合预测。在 CNN 中,过滤器值将在训练过程中进行调整。网络将自行学习如何选择合适的过滤器(特征图),以产生最佳结果。我们还将看到,由于计算复杂性,CNN 的效率和性能变成了一个挑战。在这个配方中,我们将训练并评估我们的 CNN 模型。

如何实现...

  1. 使用 ImageRecordReader 加载并初始化训练数据:
ImageRecordReader imageRecordReader = new ImageRecordReader(imageHeight,imageWidth,channels,parentPathLabelGenerator);
 imageRecordReader.initialize(trainData,null);
  1. 使用 RecordReaderDataSetIterator 创建数据集迭代器:
DataSetIterator dataSetIterator = new RecordReaderDataSetIterator(imageRecordReader,batchSize,1,numLabels);
  1. 将归一化器添加到数据集迭代器中:
DataNormalization scaler = new ImagePreProcessingScaler(0,1);
 scaler.fit(dataSetIterator);
 dataSetIterator.setPreProcessor(scaler);

  1. 通过调用 fit() 来训练模型:
MultiLayerConfiguration config = builder.build();
 MultiLayerNetwork model = new MultiLayerNetwork(config);
 model.init();
 model.setListeners(new ScoreIterationListener(100));
 model.fit(dataSetIterator,epochs);
  1. 再次训练模型,使用图像转换:
imageRecordReader.initialize(trainData,transform);
 dataSetIterator = new RecordReaderDataSetIterator(imageRecordReader,batchSize,1,numLabels);
 scaler.fit(dataSetIterator);
 dataSetIterator.setPreProcessor(scaler);
 model.fit(dataSetIterator,epochs);
  1. 评估模型并观察结果:
Evaluation evaluation = model.evaluate(dataSetIterator);
 System.out.println(evaluation.stats()); 

评估指标将显示如下:

  1. 通过添加以下依赖项来支持 GPU 加速环境:
<dependency>
  <groupId>org.nd4j</groupId>
  <artifactId>nd4j-cuda-9.1-platform</artifactId>
  <version>1.0.0-beta3</version>
 </dependency>

 <dependency>
  <groupId>org.deeplearning4j</groupId>
  <artifactId>deeplearning4j-cuda-9.1</artifactId>
  <version>1.0.0-beta3</version>
 </dependency>

它是如何工作的...

第一步中包含的参数如下:

  • parentPathLabelGenerator—在数据提取阶段创建(请参见本章的 从磁盘提取图像 配方)。

  • channels—颜色通道的数量(默认值 = 3,即 RGB)。

  • ImageRecordReader(imageHeight, imageWidth, channels, parentPathLabelGenerator)—将实际图像调整为指定大小 (imageHeight, imageWidth),以减少数据加载的工作量。

  • initialize() 方法中的 null 属性表示我们没有对转换后的图像进行训练。

在第 3 步中,我们使用ImagePreProcessingScaler进行最小-最大归一化。注意,我们需要同时使用fit()setPreProcessor()来对数据应用归一化。

对于 GPU 加速的环境,我们可以在第 4 步中使用PerformanceListener替代ScoreIterationListener,进一步优化训练过程。PerformanceListener跟踪每次迭代的训练时间,而ScoreIterationListener每隔N次迭代报告一次网络的分数。确保按照第 7 步添加 GPU 依赖项。

在第 5 步,我们再次使用之前创建的图像变换来训练模型。确保对变换后的图像也进行归一化处理。

还有更多...

我们的 CNN 模型的准确率大约为 50%。我们使用 396 张图像,涵盖 4 个类别,训练了神经网络。对于一台配备 8GB RAM 的 i7 处理器,训练完成需要 15-30 分钟。这可能会根据与训练实例并行运行的其他应用程序有所变化。训练时间也会根据硬件的质量有所不同。如果你使用更多的图像进行训练,将会观察到更好的评估指标。更多的数据有助于提高预测准确性。当然,这也需要更长的训练时间。

另一个重要的方面是实验隐藏层和子采样/卷积层的数量,以获得最佳结果。层数过多可能导致过拟合,因此,你必须通过尝试不同层数的网络配置来实验。不要为stride添加过大的值,也不要为图像设置过小的尺寸。这可能会导致过度下采样,从而导致特征丧失。

我们还可以尝试不同的权重值或权重在神经元之间的分配方式,并测试不同的梯度归一化策略,应用 L2 正则化和丢弃法。选择 L1/L2 正则化或丢弃法的常数值并没有固定的经验法则。然而,L2 正则化常数通常较小,因为它迫使权重衰减到零。神经网络通常可以安全地接受 10-20%的丢弃率,超过此范围可能会导致欠拟合。没有一个固定的常数值适用于所有情况,因为它会根据具体情况而有所不同:

GPU 加速的环境将有助于减少训练时间。DL4J 支持 CUDA,并且可以通过使用 cuDNN 进一步加速。大多数二维 CNN 层(如ConvolutionLayerSubsamplingLayer)都支持 cuDNN。

NVIDIA CUDA 深度神经网络cuDNN)库是一个针对深度学习网络的 GPU 加速原语库。你可以在这里阅读更多关于 cuDNN 的信息:developer.nvidia.com/cudnn

创建图像分类器的 API 端点

我们希望将图像分类器作为 API 在外部应用程序中使用。API 可以被外部访问,并且预测结果可以在不进行任何设置的情况下获取。在本食谱中,我们将为图像分类器创建一个 API 端点。

如何操作...

  1. 使用ModelSerializer持久化模型:
File file = new File("cnntrainedmodel.zip");
 ModelSerializer.writeModel(model,file,true);
 ModelSerializer.addNormalizerToModel(file,scaler);
  1. 使用ModelSerializer恢复训练好的模型,以执行预测:
MultiLayerNetwork network = ModelSerializer.restoreMultiLayerNetwork(modelFile);
 NormalizerStandardize normalizerStandardize = ModelSerializer.restoreNormalizerFromFile(modelFile);
  1. 设计一个 API 方法,接受用户输入并返回结果。一个示例 API 方法如下所示:
public static INDArray generateOutput(File file) throws IOException, InterruptedException {
 final File modelFile = new File("cnnmodel.zip");
 final MultiLayerNetwork model = ModelSerializer.restoreMultiLayerNetwork(modelFile);
 final RecordReader imageRecordReader = generateReader(file);
 final NormalizerStandardize normalizerStandardize = ModelSerializer.restoreNormalizerFromFile(modelFile);
 final DataSetIterator dataSetIterator = new RecordReaderDataSetIterator.Builder(imageRecordReader,1).build();
 normalizerStandardize.fit(dataSetIterator);
 dataSetIterator.setPreProcessor(normalizerStandardize);
 return model.output(dataSetIterator);
 }

  1. 创建一个 URI 映射来处理客户端请求,如下所示:
@GetMapping("/")
 public String main(final Model model){
 model.addAttribute("message", "Welcome to Java deep learning!");
 return "welcome";
 }

 @PostMapping("/")
 public String fileUpload(final Model model, final @RequestParam("uploadFile")MultipartFile multipartFile) throws IOException, InterruptedException {
 final List<String> results = cookBookService.generateStringOutput(multipartFile);
 model.addAttribute("message", "Welcome to Java deep learning!");
 model.addAttribute("results",results);
 return "welcome";
 }
  1. 构建一个cookbookapp-cnn项目,并将 API 依赖项添加到你的 Spring 项目中:
<dependency>
 <groupId>com.javadeeplearningcookbook.app</groupId>
 <artifactId>cookbookapp-cnn</artifactId>
 <version>1.0-SNAPSHOT</version>
 </dependency>
  1. 在服务层创建generateStringOutput()方法来提供 API 内容:
@Override
 public List<String> generateStringOutput(MultipartFile multipartFile) throws IOException, InterruptedException {
 //TODO: MultiPartFile to File conversion (multipartFile -> convFile)
 INDArray indArray = ImageClassifierAPI.generateOutput(convFile);

 for(int i=0; i<indArray.rows();i++){
           for(int j=0;j<indArray.columns();j++){
                   DecimalFormat df2 = new DecimalFormat("#.####");
                   results.add(df2.format(indArray.getDouble(i,j)*100)+"%"); 
                //Later add them from list to the model display on UI.
            }            
        }
  convFile.deleteOnExit();
   return results;
 }

  1. 下载并安装 Google Cloud SDK:cloud.google.com/sdk/

  2. 在 Google Cloud 控制台运行以下命令,安装 Cloud SDK 的app-engine-java组件:

gcloud components install app-engine-java
  1. 使用以下命令登录并配置 Cloud SDK:
gcloud init
  1. pom.xml中添加以下 Maven App Engine 依赖项:
<plugin>
 <groupId>com.google.cloud.tools</groupId>
 <artifactId>appengine-maven-plugin</artifactId>
 <version>2.1.0</version>
 </plugin>
  1. 根据 Google Cloud 文档,在你的项目中创建app.yaml文件:

    cloud.google.com/appengine/docs/flexible/java/configuring-your-app-with-app-yaml

  2. 导航到 Google App Engine 并点击“创建应用程序”按钮:

  1. 选择一个地区并点击“创建应用”:

  1. 选择 Java 并点击“下一步”按钮:

现在,你的应用程序引擎已经在 Google Cloud 上创建完成。

  1. 使用 Maven 构建 Spring Boot 应用程序:
mvn clean install
  1. 使用以下命令部署应用程序:
mvn appengine:deploy

工作原理...

在第 1 步和第 2 步中,我们已经将模型持久化,以便在 API 中重用模型功能。

在第 3 步中,创建一个 API 方法,接受用户输入并返回图像分类器的结果。

在第 4 步中,URI 映射将接受客户端请求(GET/POST)。GET 请求最初将提供主页,POST 请求将处理最终用户的图像分类请求。

在第 5 步中,我们将 API 依赖项添加到了pom.xml文件中。为了演示目的,我们构建了 API 的 JAR 文件,且该 JAR 文件存储在本地 Maven 仓库中。对于生产环境,你需要将你的 API(JAR 文件)提交到私有仓库,以便 Maven 可以从那里获取。

在第 6 步中,我们在 Spring Boot 应用程序的服务层调用了 ImageClassifier API,以获取结果并将其返回给控制器类。

在上一章中,我们为了演示目的将应用程序部署到本地。在本章中,我们已将应用程序部署到 Google Cloud。第 7 到 16 步专门介绍了如何在 Google Cloud 中进行部署。

我们使用了 Google App Engine,虽然我们也可以通过 Google Compute Engine 或 Dataproc 以更定制化的方式进行相同的配置。Dataproc 旨在将你的应用部署到 Spark 分布式环境中。

一旦部署成功,你应该能看到类似以下内容:

当你点击 URL(以https://xx.appspot.com开头),你应该能够看到一个网页(与上一章中的相同),用户可以在该网页上上传图片进行图像分类。

第五章:实现自然语言处理

本章将讨论 DL4J 中的词向量(Word2Vec)和段落向量(Doc2Vec)。我们将逐步开发一个完整的运行示例,涵盖所有阶段,如 ETL、模型配置、训练和评估。Word2Vec 和 Doc2Vec 是 DL4J 中的自然语言处理NLP)实现。在讨论 Word2Vec 之前,值得简单提一下词袋模型算法。

词袋模型是一种计数文档中词汇出现次数的算法。这将使我们能够执行文档分类。词袋模型和 Word2Vec 只是两种不同的文本分类方法。Word2Vec可以利用从文档中提取的词袋来创建向量。除了这些文本分类方法之外,词频-逆文档频率TF-IDF)可以用来判断文档的主题/上下文。在 TF-IDF 的情况下,将计算所有单词的分数,并将词频替换为该分数。TF-IDF 是一种简单的评分方案,但词嵌入可能是更好的选择,因为词嵌入可以捕捉到语义相似性。此外,如果你的数据集较小且上下文是特定领域的,那么词袋模型可能比 Word2Vec 更适合。

Word2Vec 是一个两层神经网络,用于处理文本。它将文本语料库转换为向量。

请注意,Word2Vec 并不是一个深度神经网络DNN)。它将文本数据转化为 DNN 可以理解的数字格式,从而实现定制化。

我们甚至可以将 Word2Vec 与 DNN 结合使用来实现这一目的。它不会通过重建训练输入词;相反,它使用语料库中的邻近词来训练词汇。

Doc2Vec(段落向量)将文档与标签关联,它是 Word2Vec 的扩展。Word2Vec 尝试将词与词相关联,而 Doc2Vec(段落向量)则将词与标签相关联。一旦我们将文档表示为向量格式,就可以将这些格式作为输入提供给监督学习算法,将这些向量映射到标签。

本章将涵盖以下几种方法:

  • 读取和加载文本数据

  • 对数据进行分词并训练模型

  • 评估模型

  • 从模型生成图形

  • 保存和重新加载模型

  • 导入 Google News 向量

  • 排查问题和调整 Word2Vec 模型

  • 使用 Word2Vec 进行基于 CNN 的句子分类

  • 使用 Doc2Vec 进行文档分类

技术要求

本章讨论的示例可以在github.com/PacktPublishing/Java-Deep-Learning-Cookbook/tree/master/05_Implementing_NLP/sourceCode/cookbookapp/src/main/java/com/javadeeplearningcookbook/examples找到。

克隆我们的 GitHub 仓库后,导航到名为 Java-Deep-Learning-Cookbook/05_Implementing_NLP/sourceCode 的目录。然后,通过导入 pom.xml 将 cookbookapp 项目作为 Maven 项目导入。

要开始使用 DL4J 中的 NLP,请在 pom.xml 中添加以下 Maven 依赖:

<dependency>
 <groupId>org.deeplearning4j</groupId>
 <artifactId>deeplearning4j-nlp</artifactId>
 <version>1.0.0-beta3</version>
 </dependency>

数据要求

项目目录中有一个 resource 文件夹,其中包含用于 LineIterator 示例所需的数据:

对于 CnnWord2VecSentenceClassificationExample 或 GoogleNewsVectorExampleYou,你可以从以下网址下载数据集:

请注意,IMDB 评论数据需要提取两次才能获得实际的数据集文件夹。

对于t-分布随机邻域嵌入t-SNE)可视化示例,所需的数据(words.txt)可以在项目根目录中找到。

读取和加载文本数据

我们需要加载原始文本格式的句子,并使用一个下划线迭代器来迭代它们。文本语料库也可以进行预处理,例如转换为小写。在配置 Word2Vec 模型时,可以指定停用词。在本教程中,我们将从各种数据输入场景中提取并加载文本数据。

准备就绪

根据你要加载的数据类型和加载方式,从第 1 步到第 5 步选择一个迭代器方法。

如何做...

  1. 使用 BasicLineIterator 创建句子迭代器:
File file = new File("raw_sentences.txt");
SentenceIterator iterator = new BasicLineIterator(file);

例如,访问 github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/05_Implementing_NLP/sourceCode/cookbookapp/src/main/java/com/javadeeplearningcookbook/examples/BasicLineIteratorExample.java

  1. 使用 LineSentenceIterator 创建句子迭代器:
File file = new File("raw_sentences.txt");
SentenceIterator iterator = new LineSentenceIterator(file);

例如,访问github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/05_Implementing_NLP/sourceCode/cookbookapp/src/main/java/com/javadeeplearningcookbook/examples/LineSentenceIteratorExample.java

  1. 使用 CollectionSentenceIterator 创建句子迭代器:
List<String> sentences= Arrays.asList("sample text", "sample text", "sample text");
SentenceIterator iter = new CollectionSentenceIterator(sentences); 

查看示例,请访问github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/05_Implementing_NLP/sourceCode/cookbookapp/src/main/java/com/javadeeplearningcookbook/examples/CollectionSentenceIteratorExample.java

  1. 使用FileSentenceIterator创建一个句子迭代器:
SentenceIterator iter = new FileSentenceIterator(new File("/home/downloads/sentences.txt"));

查看示例,请访问github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/05_Implementing_NLP/sourceCode/cookbookapp/src/main/java/com/javadeeplearningcookbook/examples/FileSentenceIteratorExample.java

  1. 使用UimaSentenceIterator创建一个句子迭代器。

添加以下 Maven 依赖:

<dependency>
 <groupId>org.deeplearning4j</groupId>
 <artifactId>deeplearning4j-nlp-uima</artifactId>
 <version>1.0.0-beta3</version>
 </dependency>

然后使用迭代器,如下所示:

SentenceIterator iterator = UimaSentenceIterator.create("path/to/your/text/documents"); 

你也可以像这样使用它:

SentenceIterator iter = UimaSentenceIterator.create("path/to/your/text/documents");

查看示例,请访问github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/05_Implementing_NLP/sourceCode/cookbookapp/src/main/java/com/javadeeplearningcookbook/examples/UimaSentenceIteratorExample.java

  1. 将预处理器应用到文本语料库:
iterator.setPreProcessor(new SentencePreProcessor() {
 @Override
 public String preProcess(String sentence) {
 return sentence.toLowerCase();
 }
 });

查看示例,请访问github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/05_Implementing_NLP/sourceCode/cookbookapp/src/main/java/com/javadeeplearningcookbook/examples/SentenceDataPreProcessor.java

它是如何工作的……

在第 1 步中,我们使用了BasicLineIterator,这是一个基础的单行句子迭代器,没有涉及任何自定义。

在第 2 步中,我们使用LineSentenceIterator来遍历多句文本数据。这里每一行都被视为一个句子。我们可以用它来处理多行文本。

在第 3 步中,CollectionSentenceIterator将接受一个字符串列表作为文本输入,每个字符串表示一个句子(文档)。这可以是一个包含推文或文章的列表。

在第 4 步中,FileSentenceIterator处理文件/目录中的句子。每个文件的句子将逐行处理。

对于任何复杂的情况,我们建议使用UimaSentenceIterator,它是一个适当的机器学习级别管道。它会遍历一组文件并分割句子。 UimaSentenceIterator 管道可以执行分词、词形还原和词性标注。其行为可以根据传递的分析引擎进行自定义。这个迭代器最适合复杂数据,比如来自 Twitter API 的数据。分析引擎是一个文本处理管道。

如果你想在遍历一次后重新开始迭代器的遍历,你需要使用 reset() 方法。

我们可以通过在数据迭代器上定义预处理器来规范化数据并移除异常。因此,在步骤 5 中,我们定义了一个归一化器(预处理器)。

还有更多...

我们还可以通过传递分析引擎来使用 UimaSentenceIterator 创建句子迭代器,代码如下所示:

SentenceIterator iterator = new UimaSentenceIterator(path,AnalysisEngineFactory.createEngine( AnalysisEngineFactory.createEngineDescription(TokenizerAnnotator.getDescription(), SentenceAnnotator.getDescription())));

分析引擎的概念借鉴自 UIMA 的文本处理管道。DL4J 提供了用于常见任务的标准分析引擎,支持进一步的文本自定义并决定句子的定义方式。分析引擎是线程安全的,相较于 OpenNLP 的文本处理管道。基于 ClearTK 的管道也被用来处理 DL4J 中常见的文本处理任务。

另请参见

分词数据并训练模型

我们需要执行分词操作以构建 Word2Vec 模型。句子(文档)的上下文是由其中的单词决定的。Word2Vec 模型需要的是单词而非句子(文档)作为输入,因此我们需要将句子拆分为原子单元,并在每次遇到空格时创建一个令牌。DL4J 拥有一个分词器工厂,负责创建分词器。 TokenizerFactory 为给定的字符串生成分词器。在这个教程中,我们将对文本数据进行分词,并在其上训练 Word2Vec 模型。

如何操作...

  1. 创建分词器工厂并设置令牌预处理器:
TokenizerFactory tokenFactory = new DefaultTokenizerFactory();
tokenFactory.setTokenPreProcessor(new CommonPreprocessor());

  1. 将分词器工厂添加到 Word2Vec 模型配置中:
Word2Vec model = new Word2Vec.Builder()
 .minWordFrequency(wordFrequency)
 .layerSize(numFeatures)
 .seed(seed)
 .epochs(numEpochs)
 .windowSize(windowSize)
 .iterate(iterator)
 .tokenizerFactory(tokenFactory)
 .build();

  1. 训练 Word2Vec 模型:
model.fit();

如何运作...

在步骤 1 中,我们使用了 DefaultTokenizerFactory() 来创建分词器工厂,用于将单词进行分词。 这是 Word2Vec 的默认分词器,它基于字符串分词器或流分词器。我们还使用了 CommonPreprocessor 作为令牌预处理器。预处理器会从文本语料库中移除异常。 CommonPreprocessor 是一个令牌预处理器实现,它移除标点符号并将文本转换为小写。它使用 toLowerCase(String) 方法,其行为取决于默认区域设置。

以下是我们在步骤 2 中所做的配置:

  • minWordFrequency():这是词语在文本语料库中必须出现的最小次数。在我们的示例中,如果一个词出现次数少于五次,那么它将不会被学习。词语应在文本语料库中出现多次,以便模型能够学习到关于它们的有用特征。在非常大的文本语料库中,适当提高词语出现次数的最小值是合理的。

  • layerSize():这定义了词向量中的特征数量。它等同于特征空间的维度数。用 100 个特征表示的词会成为 100 维空间中的一个点。

  • iterate():这指定了训练正在进行的批次。我们可以传入一个迭代器,将其转换为词向量。在我们的例子中,我们传入了一个句子迭代器。

  • epochs():这指定了整个训练语料库的迭代次数。

  • windowSize():这定义了上下文窗口的大小。

还有更多内容……

以下是 DL4J Word2Vec 中可用的其他词法分析器工厂实现,用于为给定输入生成词法分析器:

  • NGramTokenizerFactory:这是一个基于n-gram 模型创建词法分析器的工厂。N-grams 是由文本语料库中的连续单词或字母组成,长度为n

  • PosUimaTokenizerFactory:这是一个创建词法分析器的工厂,能够过滤部分词性标注。

  • UimaTokenizerFactory:这是一个使用 UIMA 分析引擎进行词法分析的工厂。该分析引擎对非结构化信息进行检查、发现并表示语义内容。非结构化信息包括但不限于文本文件。

以下是 DL4J 中内置的词元预处理器(不包括CommonPreprocessor):

  • EndingPreProcessor:这是一个去除文本语料库中词尾的预处理器——例如,它去除词尾的sed.lying

  • LowCasePreProcessor:这是一个将文本转换为小写格式的预处理器。

  • StemmingPreprocessor:该词法分析器预处理器实现了从CommonPreprocessor继承的基本清理,并对词元执行英文 Porter 词干提取。

  • CustomStemmingPreprocessor:这是一个词干预处理器,兼容不同的词干处理程序,例如 lucene/tartarus 定义的SnowballProgram,如RussianStemmerDutchStemmerFrenchStemmer。这意味着它适用于多语言词干化。

  • EmbeddedStemmingPreprocessor:该词法分析器预处理器使用给定的预处理器并在其基础上对词元执行英文 Porter 词干提取。

我们也可以实现自己的词元预处理器——例如,一个移除所有停用词的预处理器。

评估模型

我们需要在评估过程中检查特征向量的质量。这将帮助我们了解生成的 Word2Vec 模型的质量。在本食谱中,我们将采用两种不同的方法来评估 Word2Vec 模型。

如何操作...

  1. 找到与给定词语相似的词:
Collection<String> words = model.wordsNearest("season",10); 

您将看到类似以下的n输出:

week
game
team
year
world
night
time
country
last
group
  1. 找到给定两个词的余弦相似度:
double cosSimilarity = model.similarity("season","program");
System.out.println(cosSimilarity);

对于前面的示例,余弦相似度的计算方法如下:

0.2720930874347687

它是如何工作的...

在第一步中,我们通过调用wordsNearest(),提供输入和数量n,找到了与给定词语上下文最相似的前n个词。n的数量是我们希望列出的词数。

在第二步中,我们尝试找出两个给定词语的相似度。为此,我们实际上计算了这两个给定词语之间的余弦相似度。余弦相似度是我们用来衡量词语/文档相似度的有用度量之一。我们使用训练好的模型将输入词语转化为向量。

还有更多...

余弦相似度是通过计算两个非零向量之间的角度余弦值来度量相似度的。这个度量方法衡量的是方向性,而不是大小,因为余弦相似度计算的是文档向量之间的角度,而不是词频。如果角度为零,那么余弦值将达到 1,表示它们非常相似。如果余弦相似度接近零,则表示文档之间的相似度较低,文档向量将是正交(垂直)关系。此外,彼此不相似的文档会产生负的余弦相似度。对于这些文档,余弦相似度可能会达到-1,表示文档向量之间的角度为 180 度。

从模型生成图表

我们已经提到,在训练 Word2Vec 模型时,我们使用了100的层大小。这意味着可以有 100 个特征,并最终形成一个 100 维的特征空间。我们无法绘制一个 100 维的空间,因此我们依赖于 t-SNE 进行降维。在本食谱中,我们将从 Word2Vec 模型中生成 2D 图表。

准备工作

对于这个配方,请参考以下 t-SNE 可视化示例://github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/05_Implementing_NLP/sourceCode/cookbookapp/src/main/java/com/javadeeplearningcookbook/examples/TSNEVisualizationExample.java

示例将在 CSV 文件中生成 t-SNE 图表。

如何操作...

  1. 在源代码的开头添加以下代码片段,以设置当前 JVM 运行时的数据类型:
Nd4j.setDataType(DataBuffer.Type.DOUBLE);
  1. 将词向量写入文件:
WordVectorSerializer.writeWordVectors(model.lookupTable(),new File("words.txt"));
  1. 使用WordVectorSerializer将唯一单词的权重分离成自己的列表:
Pair<InMemoryLookupTable,VocabCache> vectors = WordVectorSerializer.loadTxt(new File("words.txt"));
VocabCache cache = vectors.getSecond();
INDArray weights = vectors.getFirst().getSyn0(); 

  1. 创建一个列表来添加所有独特的词:
 List<String> cacheList = new ArrayList<>();
 for(int i=0;i<cache.numWords();i++){
 cacheList.add(cache.wordAtIndex(i));
 }
  1. 使用 BarnesHutTsne 构建一个双树 t-SNE 模型来进行降维:
BarnesHutTsne tsne = new BarnesHutTsne.Builder()
 .setMaxIter(100)
 .theta(0.5)
 .normalize(false)
 .learningRate(500)
 .useAdaGrad(false)
 .build();

  1. 建立 t-SNE 值并将其保存到文件:
tsne.fit(weights);
tsne.saveAsFile(cacheList,"tsne-standard-coords.csv");

如何操作...

在第 2 步中,来自训练模型的词向量被保存到本地计算机,以便进一步处理。

在第 3 步中,我们使用 WordVectorSerializer 从所有独特的词向量中提取数据。基本上,这将从提到的输入词汇中加载一个内存中的 VocabCache。但它不会将整个词汇表/查找表加载到内存中,因此能够处理通过网络传输的大型词汇表。

VocabCache 管理存储 Word2Vec 查找表所需的信息。我们需要将标签传递给 t-SNE 模型,而标签就是通过词向量表示的词。

在第 4 步中,我们创建了一个列表来添加所有独特的词。

BarnesHutTsne 短语是 DL4J 实现类,用于双树 t-SNE 模型。Barnes–Hut 算法采用双树近似策略。建议使用其他方法,如 主成分分析 (PCA) 或类似方法,将维度降到最多 50。

在第 5 步中,我们使用 BarnesHutTsne 设计了一个 t-SNE 模型。这个模型包含以下组件:

  • theta():这是 Barnes–Hut 平衡参数。

  • useAdaGrad():这是在自然语言处理应用中使用的传统 AdaGrad 实现。

一旦 t-SNE 模型设计完成,我们可以使用从词汇中加载的权重来拟合它。然后,我们可以将特征图保存到 Excel 文件中,如第 6 步所示。

特征坐标将如下所示:

我们可以使用 gnuplot 或任何其他第三方库来绘制这些坐标。DL4J 还支持基于 JFrame 的可视化。

保存并重新加载模型

模型持久化是一个关键话题,尤其是在与不同平台操作时。我们还可以重用该模型进行进一步的训练(迁移学习)或执行任务。

在本示例中,我们将持久化(保存和重新加载)Word2Vec 模型。

如何操作...

  1. 使用 WordVectorSerializer 保存 Word2Vec 模型:
WordVectorSerializer.writeWord2VecModel(model, "model.zip");
  1. 使用 WordVectorSerializer 重新加载 Word2Vec 模型:
Word2Vec word2Vec = WordVectorSerializer.readWord2VecModel("model.zip");

如何操作...

在第 1 步中,writeWord2VecModel() 方法将 Word2Vec 模型保存为压缩的 ZIP 文件,并将其发送到输出流。它保存了完整的模型,包括 Syn0Syn1Syn0 是存储原始词向量的数组,并且是一个投影层,可以将词的独热编码转换为正确维度的密集嵌入向量。Syn1 数组代表模型的内部隐含权重,用于处理输入/输出。

在第 2 步中,readWord2VecModel() 方法加载以下格式的模型:

  • 二进制模型,可以是压缩的或未压缩的

  • 流行的 CSV/Word2Vec 文本格式

  • DL4J 压缩格式

请注意,只有权重会通过此方法加载。

导入 Google 新闻向量

Google 提供了一个预训练的大型 Word2Vec 模型,包含大约 300 万个 300 维的英文单词向量。它足够大,并且预训练后能够展示出良好的结果。我们将使用 Google 向量作为输入单词向量进行评估。运行此示例至少需要 8 GB 的 RAM。在本教程中,我们将导入 Google News 向量并进行评估。

如何操作...

  1. 导入 Google News 向量:
File file = new File("GoogleNews-vectors-negative300.bin.gz");
Word2Vec model = WordVectorSerializer.readWord2VecModel(file);
  1. 对 Google News 向量进行评估:
model.wordsNearest("season",10))

工作原理...

在步骤 1 中,使用 readWord2VecModel() 方法加载保存为压缩文件格式的预训练 Google News 向量。

在步骤 2 中,使用 wordsNearest() 方法根据正负分数查找与给定单词最相近的单词。

执行完步骤 2 后,我们应该看到以下结果:

你可以尝试使用自己的输入来测试此技术,看看不同的结果。

还有更多...

Google News 向量的压缩模型文件大小为 1.6 GB。加载和评估该模型可能需要一些时间。如果你第一次运行代码,可能会观察到 OutOfMemoryError 错误:

现在我们需要调整虚拟机(VM)选项,以便为应用程序提供更多内存。你可以在 IntelliJ IDE 中调整 VM 选项,如下图所示。你只需要确保分配了足够的内存值,并重新启动应用程序:

故障排除和调优 Word2Vec 模型

Word2Vec 模型可以进一步调整,以产生更好的结果。在内存需求较高而资源不足的情况下,可能会发生运行时错误。我们需要对它们进行故障排除,理解发生的原因并采取预防措施。在本教程中,我们将对 Word2Vec 模型进行故障排除和调优。

如何操作...

  1. 在应用程序控制台/日志中监控 OutOfMemoryError,检查是否需要增加堆空间。

  2. 检查 IDE 控制台中的内存溢出错误。如果出现内存溢出错误,请向 IDE 中添加 VM 选项,以增加 Java 堆内存。

  3. 在运行 Word2Vec 模型时,监控 StackOverflowError。注意以下错误:

这个错误可能是由于项目中不必要的临时文件造成的。

  1. 对 Word2Vec 模型进行超参数调优。你可能需要多次训练,使用不同的超参数值,如 layerSizewindowSize 等。

  2. 在代码层面推导内存消耗。根据代码中使用的数据类型及其消耗的数据量来计算内存消耗。

工作原理...

内存溢出错误通常表明需要调整 VM 选项。如何调整这些参数取决于硬件的内存容量。对于步骤 1,如果你使用的是像 IntelliJ 这样的 IDE,你可以通过 VM 属性如-Xmx-Xms等提供 VM 选项。VM 选项也可以通过命令行使用。

例如,要将最大内存消耗增加到 8 GB,你需要在 IDE 中添加-Xmx8G VM参数。

为了缓解步骤 2 中提到的StackOverflowError,我们需要删除在项目目录下由 Java 程序执行时创建的临时文件。这些临时文件应类似于以下内容:

关于步骤 3,如果你观察到你的 Word2Vec 模型未能包含原始文本数据中的所有词汇,那么你可能会考虑增加 Word2Vec 模型的 层大小 。这个layerSize实际上就是输出向量维度或特征空间维度。例如,在我们的代码中,我们的layerSize100。这意味着我们可以将其增大到更大的值,比如200,作为一种解决方法:

Word2Vec model = new Word2Vec.Builder()
 .iterate(iterator)
 .tokenizerFactory(tokenizerFactory)
 .minWordFrequency(5)
 .layerSize(200)
 .seed(42)
 .windowSize(5)
 .build();

如果你有一台 GPU 加速的机器,你可以用它来加速 Word2Vec 的训练时间。只要确保 DL4J 和 ND4J 后端的依赖项按常规添加即可。如果结果看起来仍然不对,确保没有归一化问题。

wordsNearest()等任务默认使用归一化权重,而其他任务则需要未归一化的权重。

关于步骤 4,我们可以使用传统方法。权重矩阵是 Word2Vec 中内存消耗最大的部分。其计算方法如下:

词汇数 * 维度数 * 2 * 数据类型内存占用

例如,如果我们的 Word2Vec 模型包含 100,000 个词汇,且使用 long 数据类型,100 个维度,则权重矩阵的内存占用为 100,000 * 100 * 2 * 8(long 数据类型大小)= 160 MB RAM,仅用于权重矩阵。

请注意,DL4J UI 仅提供内存消耗的高层概览。

另见

使用 Word2Vec 进行 CNN 的句子分类

神经网络需要数值输入才能按预期执行操作。对于文本输入,我们不能直接将文本数据输入神经网络。由于Word2Vec将文本数据转换为向量,因此可以利用 Word2Vec,使得我们可以将其与神经网络结合使用。我们将使用预训练的 Google News 词向量模型作为参考,并在其基础上训练 CNN 网络。完成此过程后,我们将开发一个 IMDB 评论分类器,将评论分类为正面或负面。根据论文arxiv.org/abs/1408.5882中的内容,结合预训练的 Word2Vec 模型和 CNN 会带来更好的结果。

我们将采用定制的 CNN 架构,并结合 2014 年 Yoon Kim 在其论文中建议的预训练词向量模型,arxiv.org/abs/1408.5882。该架构稍微比标准 CNN 模型更为复杂。我们还将使用两个巨大的数据集,因此应用程序可能需要相当大的 RAM 和性能基准,以确保可靠的训练时间并避免OutOfMemory错误。

在本教程中,我们将使用 Word2Vec 和 CNN 进行句子分类。

准备开始

参考示例:github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/05_Implementing_NLP/sourceCode/cookbookapp/src/main/java/com/javadeeplearningcookbook/examples/CnnWord2VecSentenceClassificationExample.java

你还应该确保通过更改 VM 选项来增加更多的 Java 堆空间——例如,如果你的 RAM 为 8GB,可以设置-Xmx2G -Xmx6G作为 VM 参数。

我们将在第一步中提取 IMDB 数据。文件结构将如下所示:

如果我们进一步进入数据集目录,你会看到它们被标记为以下内容:

如何执行...

  1. 使用WordVectorSerializer加载词向量模型:
WordVectors wordVectors = WordVectorSerializer.loadStaticModel(new File(WORD_VECTORS_PATH));

  1. 使用FileLabeledSentenceProvider创建句子提供器:
 Map<String,List<File>> reviewFilesMap = new HashMap<>();
 reviewFilesMap.put("Positive", Arrays.asList(filePositive.listFiles()));
 reviewFilesMap.put("Negative", Arrays.asList(fileNegative.listFiles()));
 LabeledSentenceProvider sentenceProvider = new FileLabeledSentenceProvider(reviewFilesMap, rndSeed); 
  1. 使用CnnSentenceDataSetIterator创建训练迭代器或测试迭代器,以加载 IMDB 评论数据:
CnnSentenceDataSetIterator iterator = new CnnSentenceDataSetIterator.Builder(CnnSentenceDataSetIterator.Format.CNN2D)
 .sentenceProvider(sentenceProvider)
 .wordVectors(wordVectors) //we mention word vectors here
 .minibatchSize(minibatchSize)
 .maxSentenceLength(maxSentenceLength) //words with length greater than this will be ignored.
 .useNormalizedWordVectors(false)
 .build();

  1. 通过添加默认的超参数来创建ComputationGraph配置:
ComputationGraphConfiguration.GraphBuilder builder = new NeuralNetConfiguration.Builder()
 .weightInit(WeightInit.RELU)
 .activation(Activation.LEAKYRELU)
 .updater(new Adam(0.01))
 .convolutionMode(ConvolutionMode.Same) //This is important so we can 'stack' the results later
 .l2(0.0001).graphBuilder();

  1. 使用addLayer()方法配置ComputationGraph的层:
builder.addLayer("cnn3", new ConvolutionLayer.Builder()
 .kernelSize(3,vectorSize) //vectorSize=300 for google vectors
 .stride(1,vectorSize)
 .nOut(100)
 .build(), "input");
 builder.addLayer("cnn4", new ConvolutionLayer.Builder()
 .kernelSize(4,vectorSize)
 .stride(1,vectorSize)
 .nOut(100)
 .build(), "input");
 builder.addLayer("cnn5", new ConvolutionLayer.Builder()
 .kernelSize(5,vectorSize)
 .stride(1,vectorSize)
 .nOut(100)
 .build(), "input");
  1. 将卷积模式设置为稍后堆叠结果:
builder.addVertex("merge", new MergeVertex(), "cnn3", "cnn4", "cnn5")

  1. 创建并初始化ComputationGraph模型:
ComputationGraphConfiguration config = builder.build();
 ComputationGraph net = new ComputationGraph(config);
  net.init();
  1. 使用fit()方法进行训练:
for (int i = 0; i < numEpochs; i++) {
 net.fit(trainIterator);
 }
  1. 评估结果:
Evaluation evaluation = net.evaluate(testIter);
System.out.println(evaluation.stats());
  1. 获取 IMDB 评论数据的预测:
INDArray features = ((CnnSentenceDataSetIterator)testIterator).loadSingleSentence(contents);
 INDArray predictions = net.outputSingle(features);
 List<String> labels = testIterator.getLabels();
 System.out.println("\n\nPredictions for first negative review:");
 for( int i=0; i<labels.size(); i++ ){
 System.out.println("P(" + labels.get(i) + ") = " + predictions.getDouble(i));
 }

它是如何工作的...

在第 1 步,我们使用 loadStaticModel() 从给定路径加载模型;但是,你也可以使用 readWord2VecModel()。与 readWord2VecModel() 不同,loadStaticModel() 使用主机内存。

在第 2 步,FileLabeledSentenceProvider 被用作数据源从文件中加载句子/文档。我们使用相同的方式创建了 CnnSentenceDataSetIteratorCnnSentenceDataSetIterator 处理将句子转换为 CNN 的训练数据,其中每个单词使用指定的词向量模型进行编码。句子和标签由 LabeledSentenceProvider 接口提供。LabeledSentenceProvider 的不同实现提供了不同的加载句子/文档及标签的方式。

在第 3 步,我们创建了 CnnSentenceDataSetIterator 来创建训练/测试数据集迭代器。我们在这里配置的参数如下:

  • sentenceProvider():将句子提供者(数据源)添加到 CnnSentenceDataSetIterator

  • wordVectors():将词向量引用添加到数据集迭代器中——例如,Google News 词向量。

  • useNormalizedWordVectors():设置是否可以使用标准化的词向量。

在第 5 步,我们为 ComputationGraph 模型创建了层。

ComputationGraph 配置是一个用于神经网络的配置对象,具有任意连接结构。它类似于多层配置,但允许网络架构具有更大的灵活性。

我们还创建了多个卷积层,并将它们按不同的滤波宽度和特征图堆叠在一起。

在第 6 步,MergeVertex 在激活这三层卷积层时执行深度连接。

一旦第 8 步之前的所有步骤完成,我们应该会看到以下评估指标:

在第 10 步,contents 指的是来自单句文档的字符串格式内容。

对于负面评论内容,在第 9 步后我们会看到以下结果:

这意味着该文档有 77.8%的概率呈现负面情绪。

还有更多内容...

使用从预训练无监督模型中提取的词向量进行初始化是提升性能的常见方法。如果你记得我们在这个示例中做过的事情,你会记得我们曾为同样的目的使用了预训练的 Google News 向量。对于 CNN,当其应用于文本而非图像时,我们将处理一维数组向量来表示文本。我们执行相同的步骤,如卷积和最大池化与特征图,正如在第四章《构建卷积神经网络》中讨论的那样。唯一的区别是,我们使用的是表示文本的向量,而不是图像像素。随后,CNN 架构在 NLP 任务中展现了出色的结果。可以在www.aclweb.org/anthology/D14-1181中找到有关此主题的进一步见解。

计算图的网络架构是一个有向无环图,其中图中的每个顶点都是一个图顶点。图顶点可以是一个层,或者是定义随机前向/反向传递功能的顶点。计算图可以有任意数量的输入和输出。我们需要叠加多个卷积层,但在普通的 CNN 架构中这是不可能的。

ComputaionGraph 提供了一个配置选项,称为 convolutionModeconvolutionMode 决定了网络配置以及卷积和下采样层的卷积操作如何执行(对于给定的输入大小)。网络配置如 stride/padding/kernelSize 适用于特定的卷积模式。我们通过设置 convolutionMode 来配置卷积模式,因为我们希望将三个卷积层的结果叠加为一个并生成预测。

卷积层和下采样层的输出尺寸在每个维度上计算方式如下:

outputSize = (inputSize - kernelSize + 2padding) / stride + 1*

如果 outputSize 不是整数,则在网络初始化或前向传递过程中将抛出异常。我们之前讨论过 MergeVertex,它用于组合两个或更多层的激活值。我们使用 MergeVertex 来执行与卷积层相同的操作。合并将取决于输入类型——例如,如果我们想要合并两个卷积层,样本大小(batchSize)为 100,并且 depth 分别为 depth1depth2,则 merge 将按以下规则叠加结果:

depth = depth1 + depth2

使用 Doc2Vec 进行文档分类

Word2Vec 将词汇与词汇相关联,而 Doc2Vec(也称为段落向量)的目的是将标签与词汇相关联。在本食谱中,我们将讨论 Doc2Vec。文档按特定方式标记,使文档根目录下的子目录表示文档标签。例如,所有与金融相关的数据应放在finance子目录下。在这个食谱中,我们将使用 Doc2Vec 进行文档分类。

如何操作...

  1. 使用FileLabelAwareIterator提取并加载数据:
LabelAwareIterator labelAwareIterator = new FileLabelAwareIterator.Builder()
 .addSourceFolder(new ClassPathResource("label").getFile()).build();  
  1. 使用TokenizerFactory创建一个分词器:
TokenizerFactory tokenizerFactory = new DefaultTokenizerFactory();
tokenizerFactory.setTokenPreProcessor(new CommonPreprocessor()); 

  1. 创建一个ParagraphVector模型定义:
ParagraphVectors paragraphVectors = new ParagraphVectors.Builder()
 .learningRate(learningRate)
 .minLearningRate(minLearningRate)
 .batchSize(batchSize)
 .epochs(epochs)
 .iterate(labelAwareIterator)
 .trainWordVectors(true)
 .tokenizerFactory(tokenizerFactory)
 .build();

  1. 通过调用fit()方法训练ParagraphVectors
paragraphVectors.fit();

  1. 为未标记的数据分配标签并评估结果:
ClassPathResource unClassifiedResource = new ClassPathResource("unlabeled");
 FileLabelAwareIterator unClassifiedIterator = new FileLabelAwareIterator.Builder()
 .addSourceFolder(unClassifiedResource.getFile())
 .build();
  1. 存储权重查找表:
InMemoryLookupTable<VocabWord> lookupTable = (InMemoryLookupTable<VocabWord>)paragraphVectors.getLookupTable();

  1. 如下伪代码所示,预测每个未分类文档的标签:
while (unClassifiedIterator.hasNextDocument()) {
//Calculate the domain vector of each document.
//Calculate the cosine similarity of the domain vector with all 
//the given labels
 //Display the results
 }

  1. 从文档中创建标记,并使用迭代器来检索文档实例:
LabelledDocument labelledDocument = unClassifiedIterator.nextDocument();
 List<String> documentAsTokens = tokenizerFactory.create(labelledDocument.getContent()).getTokens();

  1. 使用查找表来获取词汇信息(VocabCache):
VocabCache vocabCache = lookupTable.getVocab();

  1. 计算VocabCache中匹配的所有实例:
AtomicInteger cnt = new AtomicInteger(0);
 for (String word: documentAsTokens) {
 if (vocabCache.containsWord(word)){
 cnt.incrementAndGet();
 }
 }
 INDArray allWords = Nd4j.create(cnt.get(), lookupTable.layerSize());

  1. 将匹配词的词向量存储在词汇中:
cnt.set(0);
 for (String word: documentAsTokens) {
 if (vocabCache.containsWord(word))
 allWords.putRow(cnt.getAndIncrement(), lookupTable.vector(word));
 }

  1. 通过计算词汇嵌入的平均值来计算领域向量:
INDArray documentVector = allWords.mean(0);

  1. 检查文档向量与标记词向量的余弦相似度:
List<String> labels = labelAwareIterator.getLabelsSource().getLabels();
 List<Pair<String, Double>> result = new ArrayList<>();
 for (String label: labels) {
 INDArray vecLabel = lookupTable.vector(label);
 if (vecLabel == null){
 throw new IllegalStateException("Label '"+ label+"' has no known vector!");
 }
 double sim = Transforms.cosineSim(documentVector, vecLabel);
 result.add(new Pair<String, Double>(label, sim));
 }
  1. 显示结果:
 for (Pair<String, Double> score: result) {
 log.info(" " + score.getFirst() + ": " + score.getSecond());
 }

它是如何工作的...

在第 1 步中,我们使用FileLabelAwareIterator创建了数据集迭代器。

FileLabelAwareIterator是一个简单的基于文件系统的LabelAwareIterator接口。它假设你有一个或多个按以下方式组织的文件夹:

  • 一级子文件夹:标签名称

  • 二级子文件夹:该标签对应的文档

查看以下截图,了解此数据结构的示例:

在第 3 步中,我们通过添加所有必需的超参数创建了ParagraphVector。段落向量的目的是将任意文档与标签关联。段落向量是 Word2Vec 的扩展,学习将标签和词汇相关联,而 Word2Vec 将词汇与其他词汇相关联。我们需要为段落向量定义标签,才能使其正常工作。

有关第 5 步中所做内容的更多信息,请参阅以下目录结构(项目中的unlabeled目录下):

目录名称可以是随机的,不需要特定的标签。我们的任务是为这些文档找到适当的标签(文档分类)。词嵌入存储在查找表中。对于任何给定的词汇,查找表将返回一个词向量。

词汇嵌入存储在查找表中。对于任何给定的词汇,查找表将返回一个词向量。

在第 6 步中,我们通过段落向量创建了InMemoryLookupTableInMemoryLookupTable是 DL4J 中的默认词汇查找表。基本上,查找表作为隐藏层操作,词汇/文档向量作为输出。

第 8 步到第 12 步仅用于计算每个文档的领域向量。

在第 8 步中,我们使用第 2 步中创建的分词器为文档创建了令牌。在第 9 步中,我们使用第 6 步中创建的查找表来获取VocabCacheVocabCache存储了操作查找表所需的信息。我们可以使用VocabCache在查找表中查找单词。

在第 11 步中,我们将单词向量与特定单词的出现次数一起存储在一个 INDArray 中。

在第 12 步中,我们计算了这个 INDArray 的均值,以获取文档向量。

在零维度上的均值意味着它是跨所有维度计算的。

在第 13 步中,余弦相似度是通过调用 ND4J 提供的cosineSim()方法计算的。我们使用余弦相似度来计算文档向量的相似性。ND4J 提供了一个功能接口,用于计算两个领域向量的余弦相似度。vecLabel表示从分类文档中获取的标签的文档向量。然后,我们将vecLabel与我们的未标记文档向量documentVector进行比较。

在第 14 步之后,你应该看到类似于以下的输出:

我们可以选择具有更高余弦相似度值的标签。从前面的截图中,我们可以推断出第一篇文档更可能是与金融相关的内容,概率为 69.7%。第二篇文档更可能是与健康相关的内容,概率为 53.2%。

第六章:构建用于时间序列的 LSTM 网络

在本章中,我们将讨论如何构建长短期记忆LSTM)神经网络来解决医学时间序列问题。我们将使用来自 4,000 名重症监护病房ICU)患者的数据。我们的目标是通过给定的一组通用和序列特征来预测患者的死亡率。我们有六个通用特征,如年龄、性别和体重。此外,我们还有 37 个序列特征,如胆固醇水平、体温、pH 值和葡萄糖水平。每个患者都有多个针对这些序列特征的测量记录。每个患者的测量次数不同。此外,不同患者之间测量的时间间隔也有所不同。

由于数据的序列性质,LSTM 非常适合此类问题。我们也可以使用普通的递归神经网络RNN)来解决,但 LSTM 的目的是避免梯度消失和梯度爆炸。LSTM 能够捕捉长期依赖关系,因为它具有单元状态。

在本章中,我们将涵盖以下配方:

  • 提取和读取临床数据

  • 加载和转换数据

  • 构建网络的输入层

  • 构建网络的输出层

  • 训练时间序列数据

  • 评估 LSTM 网络的效率

技术要求

本章讨论的用例的具体实现可以在这里找到:github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/06_Constructing_LSTM_Network_for_time_series/sourceCode/cookbookapp-lstm-time-series/src/main/java/LstmTimeSeriesExample.java

克隆 GitHub 仓库后,进入Java-Deep-Learning-Cookbook/06_Constructing_LSTM_Network_for_time_series/sourceCode目录。然后,通过导入pom.xml,将cookbookapp-lstm-time-series项目作为 Maven 项目导入。

从这里下载临床时间序列数据:skymindacademy.blob.core.windows.net/physionet2012/physionet2012.tar.gz。该数据集来自 PhysioNet 心脏病挑战 2012。

下载后解压文件。你应该会看到以下目录结构:

特征存储在名为sequence的目录中,标签存储在名为mortality的目录中。暂时忽略其他目录。你需要在源代码中更新特征/标签的文件路径,以便运行示例。

提取和读取临床数据

ETL(提取、转换、加载的缩写)是任何深度学习问题中最重要的一步。在本方案中,我们将重点讨论数据提取,其中我们将讨论如何提取和处理临床时间序列数据。我们在前几章中了解了常规数据类型,例如普通的 CSV/文本数据和图像。现在,让我们讨论如何处理时间序列数据。我们将使用临床时间序列数据来预测患者的死亡率。

如何操作...

  1. 创建一个NumberedFileInputSplit实例,将所有特征文件合并在一起:
new NumberedFileInputSplit(FEATURE_DIR+"/%d.csv",0,3199);
  1. 创建一个NumberedFileInputSplit实例,将所有标签文件合并在一起:
new NumberedFileInputSplit(LABEL_DIR+"/%d.csv",0,3199);
  1. 为特征/标签创建记录读取器:
SequenceRecordReader trainFeaturesReader = new CSVSequenceRecordReader(1, ",");
 trainFeaturesReader.initialize(new NumberedFileInputSplit(FEATURE_DIR+"/%d.csv",0,3199));
 SequenceRecordReader trainLabelsReader = new CSVSequenceRecordReader();
 trainLabelsReader.initialize(new NumberedFileInputSplit(LABEL_DIR+"/%d.csv",0,3199));

它是如何工作的...

时间序列数据是三维的。每个样本由它自己的文件表示。列中的特征值是在不同的时间步骤上测量的,这些时间步骤由行表示。例如,在第 1 步中,我们看到了下面的快照,其中显示了时间序列数据:

每个文件代表一个不同的序列。当你打开文件时,你会看到在不同时间步骤上记录的观察值(特征),如下所示:

标签包含在一个 CSV 文件中,其中包含值0表示死亡,值1表示生存。例如,对于1.csv中的特征,输出标签位于死亡目录下的1.csv中。请注意,我们共有 4000 个样本。我们将整个数据集分为训练集和测试集,使得训练数据包含 3200 个样本,测试数据包含 800 个样本。

在第 3 步中,我们使用了NumberedFileInputSplit来读取并将所有文件(特征/标签)以编号格式合并在一起。

CSVSequenceRecordReader用于读取 CSV 格式的数据序列,其中每个序列都定义在自己的文件中。

如上图所示,第一行仅用于特征标签,需要跳过。

因此,我们创建了以下 CSV 序列读取器:

SequenceRecordReader trainFeaturesReader = new CSVSequenceRecordReader(1, ",");

加载和转换数据

数据提取阶段之后,我们需要在将数据加载到神经网络之前进行数据转换。在数据转换过程中,确保数据集中的任何非数字字段都被转换为数字字段是非常重要的。数据转换的作用不仅仅是这样。我们还可以去除数据中的噪声并调整数值。在此方案中,我们将数据加载到数据集迭代器中,并按需要转换数据。

在上一个方案中,我们将时间序列数据提取到记录读取器实例中。现在,让我们从这些实例中创建训练/测试迭代器。我们还将分析数据并在需要时进行转换。

准备就绪

在我们继续之前,请参考下面的截图中的数据集,以了解每个数据序列的样子:

首先,我们需要检查数据中是否存在任何非数值特征。我们需要将数据加载到神经网络中进行训练,并且它应该是神经网络能够理解的格式。我们有一个顺序数据集,并且看起来没有非数值值。所有 37 个特征都是数值型的。如果查看特征数据的范围,它接近于标准化格式。

它是如何做的...

  1. 使用 SequenceRecordReaderDataSetIterator 创建训练迭代器:
DataSetIterator trainDataSetIterator = new SequenceRecordReaderDataSetIterator(trainFeaturesReader,trainLabelsReader,batchSize,numberOfLabels,false, SequenceRecordReaderDataSetIterator.AlignmentMode.ALIGN_END);
  1. 使用 SequenceRecordReaderDataSetIterator 创建测试迭代器:
DataSetIterator testDataSetIterator = new SequenceRecordReaderDataSetIterator(testFeaturesReader,testLabelsReader,batchSize,numberOfLabels,false, SequenceRecordReaderDataSetIterator.AlignmentMode.ALIGN_END);

它是如何工作的...

在步骤 1 和 2 中,我们在创建训练和测试数据集的迭代器时使用了AlignmentModeAlignmentMode 处理不同长度的输入/标签(例如,一对多和多对一的情况)。以下是一些对齐模式的类型:

  • ALIGN_END:这是用于在最后一个时间步对齐标签或输入。基本上,它在输入或标签的末尾添加零填充。

  • ALIGN_START:这是用于在第一个时间步对齐标签或输入。基本上,它在输入或标签的末尾添加零填充。

  • EQUAL_LENGTH:这假设输入时间序列和标签具有相同的长度,并且所有示例的长度都相同。

  • SequenceRecordReaderDataSetIterator:这个工具帮助从传入的记录读取器生成时间序列数据集。记录读取器应基于序列数据,最适合用于时间序列数据。查看传递给构造函数的属性:

DataSetIterator testDataSetIterator = new SequenceRecordReaderDataSetIterator(testFeaturesReader,testLabelsReader,batchSize,numberOfLabels,false, SequenceRecordReaderDataSetIterator.AlignmentMode.ALIGN_END);

testFeaturesReadertestLabelsReader 分别是输入数据(特征)和标签(用于评估)的记录读取器对象。布尔属性(false)表示我们是否有回归样本。由于我们在讨论时间序列分类问题,这里为 false。对于回归数据,必须将其设置为 true

构建网络的输入层

LSTM 层将具有门控单元,能够捕捉长期依赖关系,不同于常规 RNN。让我们讨论一下如何在网络配置中添加一个特殊的 LSTM 层。我们可以使用多层网络或计算图来创建模型。

在这个示例中,我们将讨论如何为我们的 LSTM 神经网络创建输入层。在以下示例中,我们将构建一个计算图,并向其中添加自定义层。

它是如何做的...

  1. 使用 ComputationGraph 配置神经网络,如下所示:
ComputationGraphConfiguration.GraphBuilder builder = new NeuralNetConfiguration.Builder()
 .seed(RANDOM_SEED)
 .optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
 .weightInit(WeightInit.XAVIER)
 .updater(new Adam())
 .dropOut(0.9)
 .graphBuilder()
 .addInputs("trainFeatures");
  1. 配置 LSTM 层:
new LSTM.Builder()
 .nIn(INPUTS)
 .nOut(LSTM_LAYER_SIZE)
 .forgetGateBiasInit(1)
 .activation(Activation.TANH)
 .build(),"trainFeatures");
  1. 将 LSTM 层添加到 ComputationGraph 配置中:
builder.addLayer("L1", new LSTM.Builder()
 .nIn(86)
 .nOut(200)
 .forgetGateBiasInit(1)
 .activation(Activation.TANH)
 .build(),"trainFeatures");

它是如何工作的...

在步骤 1 中,我们在调用 graphBuilder() 方法后定义了一个图顶点输入,如下所示:

builder.addInputs("trainFeatures");

通过调用graphBuilder(),我们实际上是在构建一个图构建器,以创建计算图配置。

一旦 LSTM 层在步骤 3 中被添加到ComputationGraph配置中,它们将作为输入层存在于ComputationGraph配置中。我们将前面提到的图顶点输入(trainFeatures)传递给我们的 LSTM 层,如下所示:

builder.addLayer("L1", new LSTM.Builder()
     .nIn(INPUTS)
     .nOut(LSTM_LAYER_SIZE)
     .forgetGateBiasInit(1)
     .activation(Activation.TANH)
     .build(),"trainFeatures");

最后的属性trainFeatures指的是图的顶点输入。在这里,我们指定L1层为输入层。

LSTM 神经网络的主要目的是捕获数据中的长期依赖关系。tanh函数的导数在达到零值之前可以持续很长一段时间。因此,我们使用Activation.TANH作为 LSTM 层的激活函数。

forgetGateBiasInit()设置忘记门的偏置初始化。15之间的值可能有助于学习或长期依赖关系的捕获。

我们使用Builder策略来定义 LSTM 层及其所需的属性,例如nInnOut。这些是输入/输出神经元,正如我们在第三章,构建二分类深度神经网络和第四章,构建卷积神经网络中所看到的那样。我们通过addLayer方法添加 LSTM 层。

构建网络的输出层

输出层设计是配置神经网络层的最后一步。我们的目标是实现一个时间序列预测模型。我们需要开发一个时间序列分类器来预测患者的死亡率。输出层的设计应该反映这一目标。在本教程中,我们将讨论如何为我们的用例构建输出层。

如何操作...

  1. 使用RnnOutputLayer设计输出层:
new RnnOutputLayer.Builder(LossFunctions.LossFunction.MCXENT)
 .activation(Activation.SOFTMAX)
 .nIn(LSTM_LAYER_SIZE).nOut(labelCount).build()
  1. 使用addLayer()方法将输出层添加到网络配置中:
builder.addLayer("predictMortality", new RnnOutputLayer.Builder(LossFunctions.LossFunction.MCXENT)
 .activation(Activation.SOFTMAX)
 .nIn(LSTM_LAYER_SIZE).nOut(labelCount).build(),"L1");

如何工作...

在构建输出层时,注意前一个 LSTM 输入层的nOut值。这个值将作为输出层的nInnIn应该与前一个 LSTM 输入层的nOut值相同。

在步骤 1 和步骤 2 中,我们实际上是在创建一个 LSTM 神经网络,它是常规 RNN 的扩展版本。我们使用了门控单元来实现某种内部记忆,以保持长期依赖关系。为了使预测模型能够进行预测(如患者死亡率),我们需要通过输出层生成概率。在步骤 2 中,我们看到SOFTMAX被用作神经网络输出层的激活函数。这个激活函数在计算特定标签的概率时非常有用。MCXENT是 ND4J 中负对数似然误差函数的实现。由于我们使用的是负对数似然损失函数,它将在某次迭代中,当某个标签的概率值较高时,推动结果的输出。

RnnOutputLayer更像是常规前馈网络中的扩展版本输出层。我们还可以将RnnOutputLayer用于一维的 CNN 层。还有另一个输出层,叫做RnnLossLayer,其输入和输出激活相同。在RnnLossLayer的情况下,我们有三个维度,分别是[miniBatchSize, nIn, timeSeriesLength][miniBatchSize, nOut, timeSeriesLength]的形状。

请注意,我们必须指定要连接到输出层的输入层。再看看这段代码:

builder.addLayer("predictMortality", new RnnOutputLayer.Builder(LossFunctions.LossFunction.MCXENT)
 .activation(Activation.SOFTMAX)
 .nIn(LSTM_LAYER_SIZE).nOut(labelCount).build(),"L1")

我们提到过,L1层是从输入层到输出层的。

训练时间序列数据

到目前为止,我们已经构建了网络层和参数来定义模型配置。现在是时候训练模型并查看结果了。然后,我们可以检查是否可以修改任何先前定义的模型配置,以获得最佳结果。在得出第一个训练会话的结论之前,务必多次运行训练实例。我们需要观察稳定的输出,以确保性能稳定。

在这个示例中,我们将训练 LSTM 神经网络来处理加载的时间序列数据。

如何操作……

  1. 从之前创建的模型配置中创建ComputationGraph模型:
ComputationGraphConfiguration configuration = builder.build();
   ComputationGraph model = new ComputationGraph(configuration);
  1. 加载迭代器并使用fit()方法训练模型:
for(int i=0;i<epochs;i++){
   model.fit(trainDataSetIterator);
 }

你也可以使用以下方法:

model.fit(trainDataSetIterator,epochs);

然后,我们可以通过直接在fit()方法中指定epochs参数来避免使用for循环。

它是如何工作的……

在第 2 步中,我们将数据集迭代器和训练轮数传递给训练会话。我们使用了一个非常大的时间序列数据集,因此较大的轮数将导致更长的训练时间。此外,较大的轮数并不总是能保证良好的结果,甚至可能导致过拟合。所以,我们需要多次运行训练实验,以找到轮数和其他重要超参数的最佳值。最佳值是指你观察到神经网络性能最大化的界限。

实际上,我们在使用内存门控单元优化训练过程。正如我们之前在构建网络的输入层这一部分所讨论的,LSTM 非常适合在数据集中保持长期依赖关系。

评估 LSTM 网络的效率

在每次训练迭代后,通过评估模型并与一组评估指标进行比较,来衡量网络的效率。我们根据评估指标进一步优化模型,并在接下来的训练迭代中进行调整。我们使用测试数据集进行评估。请注意,我们在这个用例中执行的是二分类任务。我们预测的是患者存活的概率。对于分类问题,我们可以绘制接收者操作特征ROC)曲线,并计算曲线下面积AUC)分数来评估模型的表现。AUC 分数的范围是从 0 到 1。AUC 分数为 0 表示 100% 的预测失败,而 1 表示 100% 的预测成功。

如何实现...

  1. 使用 ROC 进行模型评估:
ROC evaluation = new ROC(thresholdSteps);
  1. 从测试数据的特征生成输出:
DataSet batch = testDataSetIterator.next();
 INDArray[] output = model.output(batch.getFeatures());
  1. 使用 ROC 评估实例,通过调用 evalTimeseries() 执行评估:
INDArray actuals = batch.getLabels();
   INDArray predictions = output[0]
   evaluation.evalTimeSeries(actuals, predictions);
  1. 通过调用 calculateAUC() 来显示 AUC 分数(评估指标):
System.out.println(evaluation.calculateAUC());

它是如何工作的...

在步骤 3 中,actuals 是测试输入的实际输出,而 predictions 是测试输入的观察输出。

评估指标基于 actualspredictions 之间的差异。我们使用 ROC 评估指标来找出这个差异。ROC 评估适用于具有输出类别均匀分布的数据集的二分类问题。预测患者死亡率只是另一个二分类难题。

ROC 的参数化构造函数中的 thresholdSteps 是用于 ROC 计算的阈值步数。当我们减少阈值时,会得到更多的正值。这提高了敏感度,意味着神经网络在对某个项进行分类时将对其类别的唯一分类信心较低。

在步骤 4 中,我们通过调用 calculateAUC() 打印了 ROC 评估指标:

evaluation.calculateAUC();

calculateAUC() 方法将计算从测试数据绘制的 ROC 曲线下的面积。如果你打印结果,你应该看到一个介于 01 之间的概率值。我们还可以调用 stats() 方法显示整个 ROC 评估指标,如下所示:

stats() 方法将显示 AUC 分数以及 AUPRC精准率/召回率曲线下面积)指标。AUPRC 是另一种性能评估指标,其中曲线表示精准率和召回率之间的权衡。对于一个具有良好 AUPRC 分数的模型,能够在较少的假阳性结果下找到正样本。

第七章:构建用于序列分类的 LSTM 神经网络

在上一章中,我们讨论了如何为多变量特征对时间序列数据进行分类。本章将创建一个长短期记忆LSTM)神经网络来对单变量时间序列数据进行分类。我们的神经网络将学习如何对单变量时间序列进行分类。我们将使用UCI(即加利福尼亚大学欧文分校)的合成控制数据,并以此为基础对神经网络进行训练。数据将有 600 个序列,每个序列之间用新行分隔,方便我们的操作。每个序列将在 60 个时间步骤上记录数据。由于这是单变量时间序列,我们的 CSV 文件中将仅包含每个记录的示例列。每个序列都是一个记录的示例。我们将把这些数据序列分成训练集和测试集,分别进行训练和评估。分类/标签的可能类别如下:

  • 正常

  • 循环的

  • 增长趋势

  • 下降趋势

  • 向上移动

  • 向下移动

本章节我们将介绍以下食谱:

  • 提取时间序列数据

  • 加载训练数据

  • 对训练数据进行归一化

  • 构建网络的输入层

  • 构建网络的输出层

  • 评估 LSTM 网络的分类输出

让我们开始吧。

技术要求

本章节的实现代码可以在github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/07_Constructing_LSTM_Neural_network_for_sequence_classification/sourceCode/cookbookapp/src/main/java/UciSequenceClassificationExample.java找到。

克隆我们的 GitHub 仓库后,导航到Java-Deep-Learning-Cookbook/07_Constructing_LSTM_Neural_network_for_sequence_classification/sourceCode目录。然后将cookbookapp项目作为 Maven 项目导入,通过导入pom.xml

从这个 UCI 网站下载数据:archive.ics.uci.edu/ml/machine-learning-databases/synthetic_control-mld/synthetic_control.data

我们需要创建目录来存储训练数据和测试数据。请参阅以下目录结构:

我们需要为训练集和测试集分别创建两个单独的文件夹,然后分别为featureslabels创建子目录:

该文件夹结构是前述数据提取的前提条件。我们在执行提取时会将特征和标签分开。

请注意,在本食谱的所有章节中,我们使用的是 DL4J 版本 1.0.0-beta 3,除了这一章。在执行我们在本章中讨论的代码时,你可能会遇到以下错误:

Exception in thread "main" java.lang.IllegalStateException: C (result) array is not F order or is a view. Nd4j.gemm requires the result array to be F order and not a view. C (result) array: [Rank: 2,Offset: 0 Order: f Shape: [10,1], stride: [1,10]]

在写作时,DL4J 的一个新版本已发布,解决了该问题。因此,我们将使用版本 1.0.0-beta 4 来运行本章中的示例。

提取时间序列数据

我们正在使用另一个时间序列的用例,但这次我们针对的是时间序列单变量序列分类。在配置 LSTM 神经网络之前,首先需要讨论 ETL。数据提取是 ETL 过程中的第一阶段。本食谱将涵盖该用例的数据提取。

如何操作...

  1. 使用编程方式对序列数据进行分类:
// convert URI to string
 final String data = IOUtils.toString(new URL(url),"utf-8");
 // Get sequences from the raw data
 final String[] sequences = data.split("\n");
 final List<Pair<String,Integer>> contentAndLabels = new ArrayList<>();
 int lineCount = 0;
 for(String sequence : sequences) {
 // Record each time step in new line
 sequence = sequence.replaceAll(" +","\n");
 // Labels: first 100 examples (lines) are label 0, second 100 examples are label 1, and so on
 contentAndLabels.add(new Pair<>(sequence, lineCount++ / 100));
 }
  1. 按照编号格式将特征/标签存储在各自的目录中:
for(Pair<String,Integer> sequencePair : contentAndLabels) {
 if(trainCount<450) {
 featureFile = new File(trainfeatureDir+trainCount+".csv");
 labelFile = new File(trainlabelDir+trainCount+".csv");
 trainCount++;
 } else {
 featureFile = new File(testfeatureDir+testCount+".csv");
 labelFile = new File(testlabelDir+testCount+".csv");
 testCount++;
 }
 }

  1. 使用FileUtils将数据写入文件:
FileUtils.writeStringToFile(featureFile,sequencePair.getFirst(),"utf-8");
FileUtils.writeStringToFile(labelFile,sequencePair.getSecond().toString(),"utf-8");

它是如何工作的...

下载后,当我们打开合成控制数据时,它将如下所示:

在前面的截图中标记了一个单独的序列。总共有 600 个序列,每个序列由新的一行分隔。在我们的示例中,我们可以将数据集划分为 450 个序列用于训练,剩下的 150 个序列用于评估。我们正试图将给定的序列分类到六个已知类别中。

请注意,这是一个单变量时间序列。记录在单个序列中的数据跨越不同的时间步。我们为每个单独的序列创建单独的文件。单个数据单元(观察值)在文件中由空格分隔。我们将空格替换为换行符,以便单个序列中每个时间步的测量值出现在新的一行中。前 100 个序列代表类别 1,接下来的 100 个序列代表类别 2,依此类推。由于我们处理的是单变量时间序列数据,因此 CSV 文件中只有一列。所以,单个特征在多个时间步上被记录。

在步骤 1 中,contentAndLabels列表将包含序列到标签的映射。每个序列代表一个标签。序列和标签一起构成一个对。

现在我们可以采用两种不同的方法来划分数据用于训练/测试:

  • 随机打乱数据,选择 450 个序列用于训练,剩下的 150 个序列用于评估/测试。

  • 将训练/测试数据集划分为类别在数据集中的分布均匀。例如,我们可以将训练数据划分为 420 个序列,每个类别有 70 个样本,共六个类别。

我们使用随机化作为一种提高神经网络泛化能力的手段。每个序列到标签的对都写入一个单独的 CSV 文件,遵循编号的文件命名规则。

在步骤 2 中,我们提到训练用的样本为 450 个,剩余的 150 个用于评估。

在步骤 3 中,我们使用了来自 Apache Commons 库的FileUtils将数据写入文件。最终的代码如下所示:

for(Pair<String,Integer> sequencePair : contentAndLabels) {
     if(trainCount<traintestSplit) {
       featureFile = new File(trainfeatureDir+trainCount+".csv");
       labelFile = new File(trainlabelDir+trainCount+".csv");
       trainCount++;
     } else {
       featureFile = new File(testfeatureDir+testCount+".csv");
       labelFile = new File(testlabelDir+testCount+".csv");
       testCount++;
     }
    FileUtils.writeStringToFile(featureFile,sequencePair.getFirst(),"utf-8");
    FileUtils.writeStringToFile(labelFile,sequencePair.getSecond().toString(),"utf-8");
 }

我们获取序列数据并将其添加到features目录中,每个序列将由一个单独的 CSV 文件表示。类似地,我们将相应的标签添加到单独的 CSV 文件中。

label 目录中的1.csv将是feature目录中1.csv特征的对应标签。

加载训练数据

数据转换通常是数据提取后的第二个阶段。我们讨论的时间序列数据没有任何非数字字段或噪音(数据已经过清理)。因此,我们可以专注于从数据中构建迭代器,并将其直接加载到神经网络中。在本食谱中,我们将加载单变量时间序列数据用于神经网络训练。我们已经提取了合成控制数据并以合适的格式存储,以便神经网络能够轻松处理。每个序列覆盖了 60 个时间步。在本食谱中,我们将把时间序列数据加载到适当的数据集迭代器中,供神经网络进行进一步处理。

它是如何做的...

  1. 创建一个SequenceRecordReader实例,从时间序列数据中提取并加载特征:
SequenceRecordReader trainFeaturesSequenceReader = new CSVSequenceRecordReader();
 trainFeaturesSequenceReader.initialize(new NumberedFileInputSplit(new File(trainfeatureDir).getAbsolutePath()+"/%d.csv",0,449));
  1. 创建一个SequenceRecordReader实例,从时间序列数据中提取并加载标签:
SequenceRecordReader trainLabelsSequenceReader = new CSVSequenceRecordReader();
 trainLabelsSequenceReader.initialize(new NumberedFileInputSplit(new File(trainlabelDir).getAbsolutePath()+"/%d.csv",0,449));
  1. 为测试和评估创建序列读取器:
SequenceRecordReader testFeaturesSequenceReader = new CSVSequenceRecordReader();
 testFeaturesSequenceReader.initialize(new NumberedFileInputSplit(new File(testfeatureDir).getAbsolutePath()+"/%d.csv",0,149));
 SequenceRecordReader testLabelsSequenceReader = new CSVSequenceRecordReader();
 testLabelsSequenceReader.initialize(new NumberedFileInputSplit(new File(testlabelDir).getAbsolutePath()+"/%d.csv",0,149));|
  1. 使用SequenceRecordReaderDataSetIterator将数据输入到我们的神经网络中:
DataSetIterator trainIterator = new SequenceRecordReaderDataSetIterator(trainFeaturesSequenceReader,trainLabelsSequenceReader,batchSize,numOfClasses);

DataSetIterator testIterator = new SequenceRecordReaderDataSetIterator(testFeaturesSequenceReader,testLabelsSequenceReader,batchSize,numOfClasses);
  1. 重写训练/测试迭代器(使用AlignmentMode)以支持不同长度的时间序列:
DataSetIterator trainIterator = new SequenceRecordReaderDataSetIterator(trainFeaturesSequenceReader,trainLabelsSequenceReader,batchSize,numOfClasses,false, SequenceRecordReaderDataSetIterator.AlignmentMode.ALIGN_END);

它是如何工作的...

我们在步骤 1 中使用了NumberedFileInputSplit。必须使用NumberedFileInputSplit从多个遵循编号文件命名规则的文件中加载数据。请参阅本食谱中的步骤 1:

SequenceRecordReader trainFeaturesSequenceReader = new CSVSequenceRecordReader();
 trainFeaturesSequenceReader.initialize(new NumberedFileInputSplit(new File(trainfeatureDir).getAbsolutePath()+"/%d.csv",0,449));

我们在前一个食谱中将文件存储为一系列编号文件。共有 450 个文件,每个文件代表一个序列。请注意,我们已经为测试存储了 150 个文件,如步骤 3 所示。

在步骤 5 中,numOfClasses指定了神经网络试图进行预测的类别数量。在我们的示例中,它是6。我们在创建迭代器时提到了AlignmentMode.ALIGN_END。对齐模式处理不同长度的输入/标签。例如,我们的时间序列数据有 60 个时间步,且只有一个标签出现在第 60 个时间步的末尾。这就是我们在迭代器定义中使用AlignmentMode.ALIGN_END的原因,如下所示:

DataSetIterator trainIterator = new SequenceRecordReaderDataSetIterator(trainFeaturesSequenceReader,trainLabelsSequenceReader,batchSize,numOfClasses,false, SequenceRecordReaderDataSetIterator.AlignmentMode.ALIGN_END);

我们还可以有时间序列数据,在每个时间步产生标签。这些情况指的是多对多的输入/标签连接。

在步骤 4 中,我们开始使用常规的创建迭代器方式,如下所示:

DataSetIterator trainIterator = new SequenceRecordReaderDataSetIterator(trainFeaturesSequenceReader,trainLabelsSequenceReader,batchSize,numOfClasses);

DataSetIterator testIterator = new SequenceRecordReaderDataSetIterator(testFeaturesSequenceReader,testLabelsSequenceReader,batchSize,numOfClasses);

请注意,这不是创建序列读取器迭代器的唯一方法。DataVec 中有多种实现可支持不同的配置。我们还可以在样本的最后时间步对输入/标签进行对齐。为此,我们在迭代器定义中添加了AlignmentMode.ALIGN_END。如果时间步长不一致,较短的时间序列将会填充至最长时间序列的长度。因此,如果有样本的时间步少于 60 步,则会将零值填充到时间序列数据中。

归一化训练数据

数据转换本身可能不会提高神经网络的效率。同一数据集中大范围和小范围的值可能会导致过拟合(模型捕捉到噪声而非信号)。为了避免这种情况,我们对数据集进行归一化,DL4J 提供了多种实现来完成这一操作。归一化过程将原始时间序列数据转换并拟合到一个确定的值范围内,例如(0, 1)。这将帮助神经网络以更少的计算量处理数据。我们在前面的章节中也讨论了归一化,表明它会减少在训练神经网络时对数据集中特定标签的偏倚。

如何操作...

  1. 创建标准归一化器并拟合数据:
DataNormalization normalization = new NormalizerStandardize();
 normalization.fit(trainIterator);
  1. 调用setPreprocessor()方法以实时规范化数据:
trainIterator.setPreProcessor(normalization);
 testIterator.setPreProcessor(normalization);

如何工作...

在第 1 步中,我们使用NormalizerStandardize来归一化数据集。NormalizerStandardize会对数据(特征)进行归一化,使其具有0的均值和1的标准差。换句话说,数据集中的所有值都将归一化到(0, 1)的范围内:

DataNormalization normalization = new NormalizerStandardize();
 normalization.fit(trainIterator);

这是 DL4J 中的标准归一化器,尽管 DL4J 中还有其他归一化器实现。还请注意,我们不需要对测试数据调用fit(),因为我们使用在训练过程中学习到的缩放参数来缩放测试数据。

我们需要像第 2 步中展示的那样,为训练/测试迭代器调用setPreprocessor()方法。一旦使用setPreprocessor()设置了归一化器,迭代器返回的数据将会自动使用指定的归一化器进行归一化。因此,重要的是在调用fit()方法时同时调用setPreprocessor()

构建网络的输入层

层配置是神经网络配置中的一个重要步骤。我们需要创建输入层来接收从磁盘加载的单变量时间序列数据。在这个示例中,我们将为我们的用例构建一个输入层。我们还将添加一个 LSTM 层作为神经网络的隐藏层。我们可以使用计算图或常规的多层网络来构建网络配置。在大多数情况下,常规多层网络就足够了;然而,我们的用例使用的是计算图。在本示例中,我们将为网络配置输入层。

如何操作...

  1. 使用默认配置配置神经网络:
NeuralNetConfiguration.Builder neuralNetConfigBuilder = new NeuralNetConfiguration.Builder();
 neuralNetConfigBuilder.seed(123);
 neuralNetConfigBuilder.weightInit(WeightInit.XAVIER);
 neuralNetConfigBuilder.updater(new Nadam());
 neuralNetConfigBuilder.gradientNormalization(GradientNormalization.ClipElementWiseAbsoluteValue);
 neuralNetConfigBuilder.gradientNormalizationThreshold(0.5);
  1. 通过调用addInputs()来指定输入层标签:
ComputationGraphConfiguration.GraphBuilder compGraphBuilder = neuralNetConfigBuilder.graphBuilder();
 compGraphBuilder.addInputs("trainFeatures");
  1. 使用addLayer()方法添加 LSTM 层:
compGraphBuilder.addLayer("L1", new LSTM.Builder().activation(Activation.TANH).nIn(1).nOut(10).build(), "trainFeatures");

工作原理...

在步骤 1 中,我们指定了默认的seed值、初始的默认权重(weightInit)、权重updater等。我们将梯度规范化策略设置为ClipElementWiseAbsoluteValue。我们还将梯度阈值设置为0.5,作为gradientNormalization策略的输入。

神经网络在每一层计算神经元的梯度。我们在标准化训练数据这一部分中已经使用标准化器对输入数据进行了标准化。需要提到的是,我们还需要对梯度值进行标准化,以实现数据准备的目标。如步骤 1 所示,我们使用了ClipElementWiseAbsoluteValue梯度标准化。它的工作方式是使梯度的绝对值不能超过阈值。例如,如果梯度阈值为 3,则值的范围为[-3, 3]。任何小于-3 的梯度值都将被视为-3,任何大于 3 的梯度值将被视为 3。范围在[-3, 3]之间的梯度值将保持不变。我们已经在网络配置中提到了梯度标准化策略和阈值,如下所示:

neuralNetConfigBuilder.gradientNormalization(GradientNormalization.ClipElementWiseAbsoluteValue);
 neuralNetConfigBuilder.gradientNormalizationThreshold(thresholdValue);

在步骤 3 中,trainFeatures标签引用了输入层标签。输入基本上是由graphBuilder()方法返回的图顶点对象。步骤 2 中指定的 LSTM 层名称(我们示例中的L1)将在配置输出层时使用。如果存在不匹配,我们的程序将在执行过程中抛出错误,表示层的配置方式导致它们断开连接。我们将在下一个教程中更深入地讨论这个问题,当时我们将设计神经网络的输出层。请注意,我们尚未在配置中添加输出层。

为网络构建输出层

输入/隐藏层设计之后的下一步是输出层设计。正如我们在前面章节中提到的,输出层应该反映你希望从神经网络中获得的输出。根据使用场景的不同,你可能需要一个分类器或回归模型。因此,输出层必须进行配置。激活函数和误差函数需要根据其在输出层配置中的使用进行合理化。本教程假设神经网络的配置已经完成到输入层定义为止。这将是网络配置中的最后一步。

如何操作...

  1. 使用setOutputs()设置输出标签:
compGraphBuilder.setOutputs("predictSequence");
  1. 使用addLayer()方法和RnnOutputLayer构造输出层:
compGraphBuilder.addLayer("predictSequence", new RnnOutputLayer.Builder(LossFunctions.LossFunction.MCXENT)
 .activation(Activation.SOFTMAX).nIn(10).nOut(numOfClasses).build(), "L1");

工作原理...

在第 1 步中,我们为输出层添加了一个predictSequence标签。请注意,在定义输出层时,我们提到了输入层的引用。在第 2 步中,我们将其指定为L1,这是在前一个步骤中创建的 LSTM 输入层。我们需要提到这一点,以避免在执行过程中因 LSTM 层与输出层之间的断开连接而导致的错误。此外,输出层的定义应该与我们在setOutput()方法中指定的层名称相同。

在第 2 步中,我们使用RnnOutputLayer构建了输出层。这个 DL4J 输出层实现用于涉及递归神经网络的使用案例。它在功能上与多层感知器中的OutputLayer相同,但输出和标签的形状调整是自动处理的。

评估 LSTM 网络的分类输出

现在我们已经配置好神经网络,下一步是启动训练实例,然后进行评估。评估阶段对训练实例非常重要。神经网络将尝试优化梯度以获得最佳结果。一个最佳的神经网络将具有良好且稳定的评估指标。因此,评估神经网络以将训练过程引导至期望的结果是很重要的。我们将使用测试数据集来评估神经网络。

在上一章中,我们探讨了时间序列二分类的一个使用案例。现在我们有六个标签进行预测。我们讨论了多种方法来提高网络的效率。在下一步骤中,我们将采用相同的方法,评估神经网络的最佳结果。

如何做...

  1. 使用init()方法初始化ComputationGraph模型配置:
ComputationGraphConfiguration configuration = compGraphBuilder.build();
   ComputationGraph model = new ComputationGraph(configuration);
 model.init();
  1. 设置分数监听器以监控训练过程:
model.setListeners(new ScoreIterationListener(20), new EvaluativeListener(testIterator, 1, InvocationType.EPOCH_END));
  1. 通过调用fit()方法启动训练实例:
model.fit(trainIterator,numOfEpochs);
  1. 调用evaluate()计算评估指标:
Evaluation evaluation = model.evaluate(testIterator);
 System.out.println(evaluation.stats());

它是如何工作的...

在第 1 步中,我们在配置神经网络的结构时使用了计算图。计算图是递归神经网络的最佳选择。我们使用多层网络得到的评估得分大约为 78%,而使用计算图时得分高达 94%。使用ComputationGraph可以获得比常规多层感知器更好的结果。ComputationGraph适用于复杂的网络结构,并且可以根据不同层的顺序进行定制。第 1 步中使用了InvocationType.EPOCH_END(分数迭代)来在测试迭代结束时调用分数迭代器。

请注意,我们为每次测试迭代调用了分数迭代器,而不是为训练集迭代调用。为了记录每次测试迭代的分数,需要通过调用setListeners()设置适当的监听器,在训练事件开始之前,如下所示:

model.setListeners(new ScoreIterationListener(20), new EvaluativeListener(testIterator, 1, InvocationType.EPOCH_END));

在第 4 步中,模型通过调用evaluate()进行了评估:

Evaluation evaluation = model.evaluate(testIterator);

我们将测试数据集以迭代器的形式传递给evaluate()方法,这个迭代器是在加载训练数据步骤中创建的。

此外,我们使用stats()方法来显示结果。对于一个有 100 个训练周期(epochs)的计算图,我们得到以下评估指标:

现在,以下是您可以执行的实验,以便进一步优化结果。

我们在示例中使用了 100 个训练周期。您可以将训练周期从 100 减少,或者将其设置为一个特定的值。注意哪个方向能带来更好的结果。当结果达到最佳时停止。我们可以在每个训练周期结束后评估一次结果,以了解我们应该朝哪个方向继续。请查看以下训练实例日志:

在前面的示例中,准确率在上一个训练周期后下降。因此,您可以决定最佳的训练周期数量。如果我们选择更大的训练周期,神经网络将仅仅记住结果,这会导致过拟合。

在最开始没有对数据进行随机化时,您可以确保六个类别在训练集中的分布是均匀的。例如,我们可以将 420 个样本用于训练,180 个样本用于测试。这样,每个类别将有 70 个样本。然后,我们可以进行随机化,并创建迭代器。请注意,在我们的示例中,我们有 450 个用于训练的样本。在这种情况下,标签/类别的分布并不是唯一的,我们完全依赖于数据的随机化。

第八章:在无监督数据上执行异常检测

在本章中,我们将使用修改版国家标准与技术研究院MNIST)数据集,通过一个简单的自编码器进行异常检测,且没有任何预训练。我们将识别给定 MNIST 数据中的离群值。离群数字可以被认为是最不典型或不正常的数字。我们将对 MNIST 数据进行编码,然后在输出层解码回来。然后,我们将计算 MNIST 数据的重建误差。

与数字值相似的 MNIST 样本将具有较低的重建误差。然后,我们将根据重建误差对它们进行排序,并使用 JFrame 窗口显示最佳样本和最差样本(离群值)。自编码器使用前馈网络构建。请注意,我们并没有进行任何预训练。我们可以在自编码器中处理特征输入,并且在任何阶段都不需要 MNIST 标签。

在本章中,我们将涵盖以下食谱:

  • 提取并准备 MNIST 数据

  • 构建输入的密集层

  • 构建输出层

  • 使用 MNIST 图像进行训练

  • 根据异常得分评估并排序结果

  • 保存生成的模型

让我们开始吧。

技术要求

本章节的代码可以在这里找到:github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/08_Performing_Anomaly_detection_on_unsupervised%20data/sourceCode/cookbook-app/src/main/java/MnistAnomalyDetectionExample.java

JFrame 特定的实现可以在这里找到:

github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/08_Performing_Anomaly_detection_on_unsupervised%20data/sourceCode/cookbook-app/src/main/java/MnistAnomalyDetectionExample.java#L134

克隆我们的 GitHub 仓库后,导航到Java-Deep-Learning-Cookbook/08_Performing_Anomaly_detection_on_unsupervised data/sourceCode目录。然后,通过导入pom.xmlcookbook-app项目作为 Maven 项目导入。

请注意,我们使用的 MNIST 数据集可以在这里找到:yann.lecun.com/exdb/mnist/

但是,我们不需要为本章下载数据集:DL4J 有一个自定义实现,允许我们自动获取 MNIST 数据。我们将在本章中使用它。

提取并准备 MNIST 数据

与有监督的图像分类任务不同,我们将对 MNIST 数据集执行异常检测任务。更重要的是,我们使用的是无监督模型,这意味着我们在训练过程中不会使用任何类型的标签。为了启动 ETL 过程,我们将提取这种无监督的 MNIST 数据并将其准备好,以便可以用于神经网络的训练。

如何操作...

  1. 使用 MnistDataSetIterator 为 MNIST 数据创建迭代器:
DataSetIterator iter = new MnistDataSetIterator(miniBatchSize,numOfExamples,binarize);
  1. 使用 SplitTestAndTrain 将基础迭代器拆分为训练/测试迭代器:
DataSet ds = iter.next();
 SplitTestAndTrain split = ds.splitTestAndTrain(numHoldOut, new Random(12345));
  1. 创建列表以存储来自训练/测试迭代器的特征集:
List<INDArray> featuresTrain = new ArrayList<>();
 List<INDArray> featuresTest = new ArrayList<>();
 List<INDArray> labelsTest = new ArrayList<>();
  1. 将之前创建的特征/标签列表填充数据:
featuresTrain.add(split.getTrain().getFeatures());
 DataSet dsTest = split.getTest();
 featuresTest.add(dsTest.getFeatures());
 INDArray indexes = Nd4j.argMax(dsTest.getLabels(),1);
 labelsTest.add(indexes);
  1. 对每个迭代器实例调用 argmax(),如果标签是多维的,则将其转换为一维数据:
while(iter.hasNext()){
 DataSet ds = iter.next();
 SplitTestAndTrain split = ds.splitTestAndTrain(80, new Random(12345)); // 80/20 split (from miniBatch = 100)
 featuresTrain.add(split.getTrain().getFeatures());
 DataSet dsTest = split.getTest();
 featuresTest.add(dsTest.getFeatures());
 INDArray indexes = Nd4j.argMax(dsTest.getLabels(),1);
 labelsTest.add(indexes);
 }

它是如何工作的...

在步骤 1 中,我们使用 MnistDataSetIterator 在一个地方提取并加载 MNIST 数据。DL4J 提供了这个专门的迭代器来加载 MNIST 数据,而无需担心自行下载数据。你可能会注意到,MNIST 数据在官方网站上是 ubyte 格式。这显然不是我们想要的格式,因此我们需要分别提取所有的图像,以便正确加载到神经网络中。

因此,在 DL4J 中拥有像 MnistDataSetIterator 这样的 MNIST 迭代器实现非常方便。它简化了处理 ubyte 格式 MNIST 数据的常见任务。MNIST 数据共有 60,000 个训练数字,10,000 个测试数字和 10 个标签。数字图像的尺寸为 28 x 28,数据的形状是扁平化格式:[ minibatch,784]。MnistDataSetIterator 内部使用 MnistDataFetcherMnistManager 类来获取 MNIST 数据并将其加载到正确的格式中。在步骤 1 中,binarizetruefalse 表示是否对 MNIST 数据进行二值化。

请注意,在步骤 2 中,numHoldOut 表示用于训练的样本数量。如果 miniBatchSize100numHoldOut80,则剩余的 20 个样本用于测试和评估。我们可以使用 DataSetIteratorSplitter 代替步骤 2 中提到的 SplitTestAndTrain 进行数据拆分。

在步骤 3 中,我们创建了列表来维护与训练和测试相关的特征和标签。它们分别用于训练和评估阶段。我们还创建了一个列表,用于存储来自测试集的标签,在测试和评估阶段将异常值与标签进行映射。这些列表在每次批次发生时都会填充一次。例如,在 featuresTrainfeaturesTest 的情况下,一个批次的特征(经过数据拆分后)由一个 INDArray 项表示。我们还使用了 ND4J 中的 argMax() 函数,它将标签数组转换为一维数组。MNIST 标签从 09 实际上只需要一维空间来表示。

在以下代码中,1 表示维度:

Nd4j.argMax(dsTest.getLabels(),1);

同时请注意,我们使用标签来映射异常值,而不是用于训练。

构建输入的密集层

神经网络设计的核心是层架构。对于自编码器,我们需要设计在前端进行编码、在另一端进行解码的密集层。基本上,我们就是通过这种方式重建输入。因此,我们需要设计我们的层结构。

让我们从配置默认设置开始设置我们的自编码器,然后进一步定义自编码器所需的输入层。记住,神经网络的输入连接数应等于输出连接数。

如何做...

  1. 使用MultiLayerConfiguration构建自编码器网络:
NeuralNetConfiguration.Builder configBuilder = new NeuralNetConfiguration.Builder();
 configBuilder.seed(12345);
 configBuilder.weightInit(WeightInit.XAVIER);
 configBuilder.updater(new AdaGrad(0.05));
 configBuilder.activation(Activation.RELU);
 configBuilder.l2(l2RegCoefficient);
 NeuralNetConfiguration.ListBuilder builder = configBuilder.list();
  1. 使用DenseLayer创建输入层:
builder.layer(new DenseLayer.Builder().nIn(784).nOut(250).build());
 builder.layer(new DenseLayer.Builder().nIn(250).nOut(10).build());

它是如何工作的...

在第 1 步中,在配置通用神经网络参数时,我们设置了默认的学习率,如下所示:

configBuilder.updater(new AdaGrad(learningRate));

Adagrad优化器基于在训练期间参数更新的频率。Adagrad基于矢量化学习率。当接收到更多更新时,学习率会较小。这对于高维度问题至关重要。因此,这个优化器非常适合我们的自编码器应用场景。

在自编码器架构中,我们在输入层执行降维。这也被称为对数据进行编码。我们希望确保从编码数据中解码出相同的特征集合。我们计算重建误差,以衡量我们与编码前的真实特征集合有多接近。在第 2 步中,我们尝试将数据从较高维度(784)编码到较低维度(10)。

构建输出层

作为最后一步,我们需要将数据从编码状态解码回原始状态。我们能否完美地重建输入?如果可以,那么一切都好。否则,我们需要计算相关的重建误差。记住,输出层的输入连接应该与前一层的输出连接相同。

如何做...

  1. 使用OutputLayer创建一个输出层:
OutputLayer outputLayer = new OutputLayer.Builder().nIn(250).nOut(784)
 .lossFunction(LossFunctions.LossFunction.MSE)
 .build();
  1. OutputLayer添加到层定义中:
builder.layer(new OutputLayer.Builder().nIn(250).nOut(784)
 .lossFunction(LossFunctions.LossFunction.MSE)
 .build());

它是如何工作的...

我们提到了均方误差MSE)作为与输出层相关的误差函数。lossFunction,在自编码器架构中,通常是 MSE。MSE 在计算重建输入与原始输入之间的接近程度时是最优的。ND4J 有一个 MSE 的实现,即LossFunction.MSE

在输出层,我们得到重建后的输入,并且它们的维度与原始输入相同。然后我们将使用误差函数来计算重建误差。在第 1 步中,我们构建了一个输出层,用于计算异常检测的重建误差。重要的是,输入和输出层的输入连接和输出连接需要保持一致。一旦定义了输出层,我们需要将其添加到一个层配置堆栈中,以此来创建神经网络的配置。在第 2 步中,我们将输出层添加到之前维护的神经网络配置构建器中。为了遵循直观的方法,我们首先创建了配置构建器,而不像这里所采用的简单方法:github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/08_Performing_Anomaly_detection_on_unsupervised%20data/sourceCode/cookbook-app/src/main/java/MnistAnomalyDetectionExample.java

你可以通过在Builder实例上调用build()方法来获取配置实例。

使用 MNIST 图像进行训练

一旦构建了各层并形成了神经网络,我们就可以启动训练过程。在训练过程中,我们会多次重建输入并评估重建误差。在之前的示例中,我们通过根据需要定义输入和输出层完成了自编码器网络配置。请注意,我们将使用自编码器进行异常检测,因此我们使用其自身的输入特征来训练网络,而不是标签。因为我们使用自编码器进行异常检测,所以我们先编码数据,然后再解码回来以衡量重建误差。基于此,我们列出 MNIST 数据中最可能的异常。

如何操作...

  1. 选择正确的训练方法。以下是训练过程中预期会发生的情况:
 Input -> Encoded Input -> Decode -> Output

所以,我们需要训练输出与输入相对应(理想情况下,输出 ~ 输入)。

  1. 使用fit()方法训练每个特征集:
int nEpochs = 30;
 for( int epoch=0; epoch<nEpochs; epoch++ ){
 for(INDArray data : featuresTrain){
 net.fit(data,data);
 }
 }

它是如何工作的...

fit()方法接受特征和标签作为第一和第二个属性。我们会将 MNIST 特征与它们自己进行重建。换句话说,我们试图在特征被编码后重新创建它们,并检查它们与实际特征的差异。在训练过程中,我们测量重建误差,并且只关注特征值。因此,输出将与输入进行验证,并类似于自编码器的功能。所以,第 1 步对于评估阶段也至关重要。

请参考以下代码块:

for(INDArray data : featuresTrain){
 net.fit(data,data);
}

这就是我们为何将自编码器训练为其自身特征(输入)的原因,在第 2 步中我们通过这种方式调用fit()net.fit(data,data)

根据异常评分评估和排序结果

我们需要计算所有特征集的重建误差。根据这个,我们会找出所有 MNIST 数字(0 到 9)的离群数据。最后,我们将在 JFrame 窗口中显示离群数据。我们还需要来自测试集的特征值用于评估。我们还需要来自测试集的标签值,标签不是用来评估的,而是用于将异常与标签关联。然后,我们可以根据每个标签绘制离群数据。标签仅用于在 JFrame 中根据相应的标签绘制离群数据。在本配方中,我们评估了训练好的自编码器模型用于 MNIST 异常检测,然后排序结果并显示出来。

如何操作...

  1. 构建一个将每个 MNIST 数字与一组(score, feature)对相关联的映射:
Map<Integer,List<Pair<Double,INDArray>>> listsByDigit = new HashMap<>();
  1. 遍历每一个测试特征,计算重建误差,生成分数-特征对用于显示具有低重建误差的样本:
for( int i=0; i<featuresTest.size(); i++ ){
 INDArray testData = featuresTest.get(i);
 INDArray labels = labelsTest.get(i);
 for( int j=0; j<testData.rows(); j++){
 INDArray example = testData.getRow(j, true);
 int digit = (int)labels.getDouble(j);
 double score = net.score(new DataSet(example,example));
 // Add (score, example) pair to the appropriate list
 List digitAllPairs = listsByDigit.get(digit);
 digitAllPairs.add(new Pair<>(score, example));
 }
 }
  1. 创建一个自定义的比较器来排序映射:
Comparator<Pair<Double, INDArray>> sortComparator = new Comparator<Pair<Double, INDArray>>() {
 @Override
 public int compare(Pair<Double, INDArray> o1, Pair<Double, INDArray> o2) {
 return Double.compare(o1.getLeft(),o2.getLeft());
 }
 };
  1. 使用Collections.sort()对映射进行排序:
for(List<Pair<Double, INDArray>> digitAllPairs : listsByDigit.values()){
 Collections.sort(digitAllPairs, sortComparator);
 }
  1. 收集最佳/最差数据,以在 JFrame 窗口中显示用于可视化:
List<INDArray> best = new ArrayList<>(50);
 List<INDArray> worst = new ArrayList<>(50);
 for( int i=0; i<10; i++ ){
 List<Pair<Double,INDArray>> list = listsByDigit.get(i);
 for( int j=0; j<5; j++ ){
 best.add(list.get(j).getRight());
 worst.add(list.get(list.size()-j-1).getRight());
 }
 }
  1. 使用自定义的 JFrame 实现进行可视化,比如MNISTVisualizer,来展示结果:
//Visualize the best and worst digits
 MNISTVisualizer bestVisualizer = new MNISTVisualizer(imageScale,best,"Best (Low Rec. Error)");
 bestVisualizer.visualize();
 MNISTVisualizer worstVisualizer = new MNISTVisualizer(imageScale,worst,"Worst (High Rec. Error)");
 worstVisualizer.visualize();

它是如何工作的...

通过步骤 1 和步骤 2,对于每个 MNIST 数字,我们维护一个(score, feature)对的列表。我们构建了一个将每个 MNIST 数字与这个列表相关联的映射。最后,我们只需要排序就可以找到最好的/最差的案例。

我们还使用了score()函数来计算重建误差:

double score = net.score(new DataSet(example,example));

在评估过程中,我们会重建测试特征,并测量它与实际特征值的差异。较高的重建误差表明存在较高比例的离群值。

在步骤 4 之后,我们应该能看到 JFrame 可视化的重建误差,如下所示:

可视化依赖于 JFrame。基本上,我们所做的是从第 1 步中创建的映射中取出N个最佳/最差的对。我们制作一个最佳/最差数据的列表,并将其传递给我们的 JFrame 可视化逻辑,以便在 JFrame 窗口中显示离群值。右侧的 JFrame 窗口表示离群数据。我们将 JFrame 的实现留到一边,因为这超出了本书的范围。完整的 JFrame 实现请参考“技术要求”部分中提到的 GitHub 源代码。

保存结果模型

模型持久化非常重要,因为它使得无需重新训练即可重复使用神经网络模型。一旦自编码器被训练用于执行离群值检测,我们就可以将模型保存到磁盘以供以后使用。我们在前一章中解释了ModelSerializer类,我们用它来保存自编码器模型。

如何操作...

  1. 使用ModelSerializer持久化模型:
File modelFile = new File("model.zip");
 ModelSerializer.writeModel(multiLayerNetwork,file, saveUpdater);
  1. 向持久化模型中添加一个标准化器:
ModelSerializer.addNormalizerToModel(modelFile,dataNormalization);

它是如何工作的...

在本章中,我们正式针对 DL4J 版本 1.0.0-beta 3。我们使用ModelSerializer将模型保存到磁盘。如果你使用的是新版本 1.0.0-beta 4,还有另一种推荐的保存模型方法,即使用MultiLayerNetwork提供的save()方法:

File locationToSave = new File("MyMultiLayerNetwork.zip");
   model.save(locationToSave, saveUpdater);

如果你希望未来训练网络,使用saveUpdater = true

还有更多内容...

要恢复网络模型,调用restoreMultiLayerNetwork()方法:

ModelSerializer.restoreMultiLayerNetwork(new File("model.zip"));

此外,如果你使用最新版本 1.0.0-beta 4,你可以使用MultiLayerNetwork提供的load()方法:

MultiLayerNetwork restored = MultiLayerNetwork.load(locationToSave, saveUpdater);

第九章:使用 RL4J 进行强化学习

强化学习是一种以目标为导向的机器学习算法,它训练智能体做出一系列决策。对于深度学习模型,我们在现有数据上训练它们,并将学习应用于新数据或未见过的数据。强化学习通过根据持续反馈调整自己的行为来展现动态学习,以最大化奖励。我们可以将深度学习引入强化学习系统,这就是深度强化学习。

RL4J 是一个与 DL4J 集成的强化学习框架。RL4J 支持两种强化学习算法:深度 Q 学习和 A3C(即异步演员-评论家智能体)。Q 学习是一种离策略强化学习算法,旨在为给定的状态寻求最佳动作。它通过采取随机动作从当前策略之外的动作中学习。在深度 Q 学习中,我们使用深度神经网络来找到最佳的 Q 值,而不是像常规 Q 学习那样使用值迭代。在本章中,我们将使用 Project Malmo 设置一个由强化学习驱动的游戏环境。Project Malmo 是一个基于 Minecraft 的强化学习实验平台。

本章将涵盖以下内容:

  • 设置 Malmo 环境及相关依赖

  • 设置数据要求

  • 配置和训练一个深度 Q 网络(DQN)智能体

  • 评估 Malmo 智能体

技术要求

本章的源代码可以在这里找到:

github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/09_Using_RL4J_for_Reinforcement%20learning/sourceCode/cookbookapp/src/main/java/MalmoExample.java

克隆我们的 GitHub 仓库后,导航到Java-Deep-Learning-Cookbook/09_Using_RL4J_for_Reinforcement learning/sourceCode目录。然后,通过导入pom.xmlcookbookapp项目作为 Maven 项目导入。

你需要设置一个 Malmo 客户端来运行源代码。首先,根据你的操作系统下载最新的 Project Malmo 版本:github.com/Microsoft/malmo/releases

要启动 Minecraft 客户端,请导航到 Minecraft 目录并运行客户端脚本:

  • 双击launchClient.bat(在 Windows 上)。

  • 在控制台上运行./launchClient.sh(无论是在 Linux 还是 macOS 上)。

如果你在 Windows 上遇到启动客户端时的问题,可以在这里下载依赖关系查看工具:lucasg.github.io/Dependencies/

然后,按照以下步骤操作:

  1. 解压并运行DependenciesGui.exe

  2. Java_Examples目录中选择MalmoJava.dll,查看类似下面所示的缺失依赖项:

如果出现任何问题,缺失的依赖项将在列表中标记出来。你需要添加缺失的依赖项,以便成功重新启动客户端。任何缺失的库/文件应该存在于PATH环境变量中。

你可以参考此处的操作系统特定构建说明:

如果一切顺利,你应该能看到类似下面的画面:

此外,你需要创建一个任务架构来构建游戏窗口的模块。完整的任务架构可以在本章节的项目目录中找到:github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/09_Using_RL4J_for_Reinforcement%20learning/sourceCode/cookbookapp/src/main/resources/cliff_walking_rl4j.xml

设置 Malmo 环境及其相关依赖

我们需要设置 RL4J Malmo 依赖项来运行源代码。就像任何其他 DL4J 应用一样,我们还需要根据硬件(CPU/GPU)添加 ND4J 后端依赖。在本教程中,我们将添加所需的 Maven 依赖并设置环境来运行应用程序。

准备工作

在我们运行 Malmo 示例源代码之前,Malmo 客户端应该已经启动并正常运行。我们的源代码将与 Malmo 客户端进行通信,以便创建并执行任务。

如何操作...

  1. 添加 RL4J 核心依赖:
<dependency>
 <groupId>org.deeplearning4j</groupId>
 <artifactId>rl4j-core</artifactId>
 <version>1.0.0-beta3</version>
 </dependency>
  1. 添加 RL4J Malmo 依赖:
<dependency>
 <groupId>org.deeplearning4j</groupId>
 <artifactId>rl4j-malmo</artifactId>
 <version>1.0.0-beta3</version>
 </dependency>
  1. 为 ND4J 后端添加依赖:

    • 对于 CPU,你可以使用以下配置:
<dependency>
 <groupId>org.nd4j</groupId>
 <artifactId>nd4j-native-platform</artifactId>
 <version>1.0.0-beta3</version>
 </dependency>
    • 对于 GPU,你可以使用以下配置:
<dependency>
 <groupId>org.nd4j</groupId>
 <artifactId>nd4j-cuda-10.0</artifactId>
 <version>1.0.0-beta3</version>
 </dependency>
  1. MalmoJavaJar添加 Maven 依赖:
<dependency>
 <groupId>com.microsoft.msr.malmo</groupId>
 <artifactId>MalmoJavaJar</artifactId>
 <version>0.30.0</version>
 </dependency>

它是如何工作的...

在第 1 步中,我们添加了 RL4J 核心依赖项,将 RL4J DQN 库引入到我们的应用程序中。在第 2 步中,添加了 RL4J Malmo 依赖项,以构建 Malmo 环境并在 RL4J 中构建任务。

我们还需要添加特定于 CPU/GPU 的 ND4J 后端依赖项(第 3 步)。最后,在第 4 步中,我们添加了MalmoJavaJar的依赖项(第 4 步),它作为 Java 程序与 Malmo 交互的通信接口。

设置数据要求

Malmo 强化学习环境的数据包括代理正在移动的图像帧。Malmo 的示例游戏窗口如下所示。这里,如果代理走过熔岩,它会死亡:

Malmo 要求开发者指定 XML 模式以生成任务。我们需要为代理和服务器创建任务数据,以便在世界中创建方块(即游戏环境)。在本示例中,我们将创建一个 XML 模式来指定任务数据。

如何执行此操作...

  1. 使用<ServerInitialConditions>标签定义世界的初始条件:
Sample:
 <ServerInitialConditions>
 <Time>
 <StartTime>6000</StartTime>
 <AllowPassageOfTime>false</AllowPassageOfTime>
 </Time>
 <Weather>clear</Weather>
 <AllowSpawning>false</AllowSpawning>
 </ServerInitialConditions>
  1. 访问www.minecraft101.net/superflat/并为超平坦世界创建您自己的预设字符串:

  1. 使用<FlatWorldGenerator>标签生成具有指定预设字符串的超平坦世界:

<FlatWorldGenerator generatorString="3;7,220*1,5*3,2;3;,biome_1"/>
  1. 使用<DrawingDecorator>标签在世界中绘制结构:
Sample:
 <DrawingDecorator>
 <!-- coordinates for cuboid are inclusive -->
 <DrawCuboid x1="-2" y1="46" z1="-2" x2="7" y2="50" z2="18" type="air" />
 <DrawCuboid x1="-2" y1="45" z1="-2" x2="7" y2="45" z2="18" type="lava" />
 <DrawCuboid x1="1" y1="45" z1="1" x2="3" y2="45" z2="12" type="sandstone" />
 <DrawBlock x="4" y="45" z="1" type="cobblestone" />
 <DrawBlock x="4" y="45" z="12" type="lapis_block" />
 <DrawItem x="4" y="46" z="12" type="diamond" />
 </DrawingDecorator>
  1. 使用<ServerQuitFromTimeUp>标签为所有代理指定时间限制:
<ServerQuitFromTimeUp timeLimitMs="100000000"/>
  1. 使用<ServerHandlers>标签将所有任务处理器添加到方块中:
<ServerHandlers>
 <FlatWorldGenerator>{Copy from step 3}</FlatWorldGenerator>
 <DrawingDecorator>{Copy from step 4}</DrawingDecorator>
 <ServerQuitFromTimeUp>{Copy from step 5}</ServerQuitFromTimeUp>
 </ServerHandlers>
  1. <ServerSection>标签下添加<ServerHandlers><ServerInitialConditions>
<ServerSection>
 <ServerInitialConditions>{Copy from step 1}</ServerInitialConditions>
 <ServerHandlers>{Copy from step 6}</ServerHandlers>
 </ServerSection>
  1. 定义代理的名称和起始位置:
Sample:
 <Name>Cristina</Name>
 <AgentStart>
   <Placement x="4.5" y="46.0" z="1.5" pitch="30" yaw="0"/>
 </AgentStart>
  1. 使用<ObservationFromGrid>标签定义方块类型:
Sample:
 <ObservationFromGrid>
  <Grid name="floor">
  <min x="-4" y="-1" z="-13"/>
  <max x="4" y="-1" z="13"/>
  </Grid>
 </ObservationFromGrid>
  1. 使用<VideoProducer>标签配置视频帧:
Sample:
 <VideoProducer viewpoint="1" want_depth="false">
 <Width>320</Width>
 <Height>240</Height>
 </VideoProducer>
  1. 提到当代理与使用<RewardForTouchingBlockType>标签的方块类型接触时将获得的奖励点数:
Sample:
 <RewardForTouchingBlockType>
 <Block reward="-100.0" type="lava" behaviour="onceOnly"/>
 <Block reward="100.0" type="lapis_block" behaviour="onceOnly"/>
 </RewardForTouchingBlockType>
  1. 提到奖励点数以向代理发出命令,使用<RewardForSendingCommand>标签:
Sample:
 <RewardForSendingCommand reward="-1"/>
  1. 使用<AgentQuitFromTouchingBlockType>标签为代理指定任务终点:
<AgentQuitFromTouchingBlockType>
  <Block type="lava" />
  <Block type="lapis_block" />
 </AgentQuitFromTouchingBlockType>
  1. <AgentHandlers>标签下添加所有代理处理器函数:
<AgentHandlers>
   <ObservationFromGrid>{Copy from step 9}</ObservationFromGrid>
   <VideoProducer></VideoProducer> // Copy from step 10
   <RewardForTouchingBlockType>{Copy from step 11}</RewardForTouchingBlockType>
   <RewardForSendingCommand> // Copy from step 12
   <AgentQuitFromTouchingBlockType>{Copy from step 13}  </AgentQuitFromTouchingBlockType>
 </AgentHandlers>
  1. 将所有代理处理器添加到<AgentSection>中:
<AgentSection mode="Survival">
     <AgentHandlers>
        {Copy from step 14}
     </AgentHandlers>
 </AgentSection>
  1. 创建一个DataManager实例来记录训练数据:
DataManager manager = new DataManager(false);

它是如何工作的...

在第 1 步中,以下配置被添加为世界的初始条件:

  • StartTime:这指定了任务开始时的时间,以千分之一小时为单位。6000 表示中午 12 点。

  • AllowPassageOfTime:如果设置为false,则会停止昼夜循环。在任务期间,天气和太阳位置将保持不变。

  • Weather:这指定了任务开始时的天气类型。

  • AllowSpawning:如果设置为true,则在任务期间将生成动物和敌对生物。

第 2 步中,我们创建了一个预设字符串来表示在第 3 步中使用的超平面类型。超平面类型指的就是任务中看到的表面类型。

在第 4 步,我们使用 DrawCuboidDrawBlock 向世界中绘制了结构。

我们遵循三维空间 (x1,y1,z1) -> (x2,y2,z2) 来指定边界。type 属性用于表示块类型。你可以为实验添加任何 198 个可用的块。

在第 6 步,我们将所有与世界创建相关的任务处理程序添加到<ServerHandlers>标签下。然后,在第 7 步,我们将它们添加到<ServerSection>父标签中。

在第 8 步,<Placement>标签用于指定玩家的起始位置。如果未指定起始点,它将随机选择。

在第 9 步,我们指定了游戏窗口中地面块的位置。在第 10 步,viewpoint 设置了相机的视角:

viewpoint=0 -> first-person
 viewpoint=1 -> behind
 viewpoint=2 -> facing

在第 13 步,我们指定了智能体移动在步骤结束后会停止的块类型。最后,我们在第 15 步的 AgentSection 标签中添加了所有特定于智能体的任务处理程序。任务架构创建将在第 15 步结束。

现在,我们需要存储来自任务的训练数据。我们使用 DataManager 来处理训练数据的记录。如果rl4j-data目录不存在,它会创建该目录,并随着强化学习训练的进行存储训练数据。我们在第 16 步创建 DataManager 时传递了 false 作为属性。这意味着我们不会持久化训练数据或模型。如果要持久化训练数据和模型,请传递 true。请注意,在配置 DQN 时,我们需要用到数据管理器实例。

另见

配置和训练 DQN 智能体

DQN 是一种强化学习的重要类别,称为价值学习。在这里,我们使用深度神经网络来学习最优 Q 值函数。在每次迭代中,网络会近似 Q 值,并根据贝尔曼方程对其进行评估,以衡量智能体的准确性。Q 值应该在智能体在世界中进行动作时得到优化。因此,如何配置 Q-learning 过程非常重要。在这个教程中,我们将配置 DQN 以进行 Malmo 任务,并训练智能体完成任务。

准备工作

以下内容的基础知识是此教程的先决条件:

  • Q-learning

  • DQN

Q-learning 基础将有助于在配置 DQN 的 Q-learning 超参数时。

如何操作...

  1. 为任务创建一个动作空间:
Sample:
 MalmoActionSpaceDiscrete actionSpace =
 new MalmoActionSpaceDiscrete("movenorth 1", "movesouth 1", "movewest 1", "moveeast 1");
 actionSpace.setRandomSeed(rndSeed);

  1. 为任务创建一个观测空间:
MalmoObservationSpace observationSpace = new MalmoObservationSpacePixels(xSize, ySize);
  1. 创建一个 Malmo 一致性策略:
MalmoDescretePositionPolicy obsPolicy = new MalmoDescretePositionPolicy();
  1. 在 Malmo Java 客户端周围创建一个 MDP(马尔可夫决策过程)包装器:
Sample:
 MalmoEnv mdp = new MalmoEnv("cliff_walking_rl4j.xml", actionSpace, observationSpace, obsPolicy);
  1. 使用DQNFactoryStdConv创建一个 DQN:
Sample:
 public static DQNFactoryStdConv.Configuration MALMO_NET = new DQNFactoryStdConv.Configuration(
 learingRate,
 l2RegParam,
 updaters,
 listeners
 );
  1. 使用HistoryProcessor对像素图像输入进行缩放:
Sample:
 public static HistoryProcessor.Configuration MALMO_HPROC = new HistoryProcessor.Configuration(
 numOfFrames,
 rescaledWidth,
 rescaledHeight,
 croppingWidth,
 croppingHeight,
 offsetX,
 offsetY,
 numFramesSkip
 );
  1. 通过指定超参数创建 Q 学习配置:
Sample:
 public static QLearning.QLConfiguration MALMO_QL = new QLearning.QLConfiguration(
 rndSeed,
 maxEpochStep,
 maxStep,
 expRepMaxSize,
 batchSize,
 targetDqnUpdateFreq,
 updateStart,
 rewardFactor,
 gamma,
 errorClamp,
 minEpsilon,
 epsilonNbStep,
 doubleDQN
 );
  1. 使用QLearningDiscreteConv创建 DQN 模型,通过传递 MDP 包装器和DataManager:在QLearningDiscreteConv构造函数中:
Sample:
 QLearningDiscreteConv<MalmoBox> dql =
 new QLearningDiscreteConv<MalmoBox>(mdp, MALMO_NET, MALMO_HPROC, MALMO_QL, manager);
  1. 训练 DQN:
dql.train();

它是如何工作的...

在第 1 步中,我们通过指定一组定义好的 Malmo 动作,为代理定义了一个动作空间。例如,movenorth 1表示将代理向北移动一个区块。我们将一个字符串列表传递给MalmoActionSpaceDiscrete,指示代理在 Malmo 空间中的动作。

在第 2 步中,我们根据输入图像(来自 Malmo 空间)的位图大小(由xSizeySize指定)创建了一个观察空间。此外,我们假设有三个颜色通道(R、G、B)。代理需要在运行之前了解观察空间。我们使用了MalmoObservationSpacePixels,因为我们目标是从像素中获取观察。

在第 3 步中,我们使用MalmoDescretePositionPolicy创建了一个 Malmo 一致性策略,以确保即将到来的观察处于一致的状态。

MDP 是强化学习中在网格世界环境中使用的一种方法。我们的任务有网格形式的状态。MDP 需要一个策略,强化学习的目标是为 MDP 找到最优策略。MalmoEnv是一个围绕 Java 客户端的 MDP 包装器。

在第 4 步中,我们使用任务架构、动作空间、观察空间和观察策略创建了一个 MDP 包装器。请注意,观察策略与代理在学习过程结束时希望形成的策略不同。

在第 5 步中,我们使用DQNFactoryStdConv通过添加卷积层构建了 DQN。

在第 6 步中,我们配置了HistoryProcessor来缩放并移除不需要的像素。HistoryProcessor的实际目的是执行经验回放,在这种回放中,代理的过去经验将在决定当前状态的动作时考虑。通过使用HistoryProcessor,我们可以将部分状态观察转变为完全观察状态,也就是说,当当前状态是先前状态的积累时。

下面是第 7 步中在创建 Q 学习配置时使用的超参数:

  • maxEpochStep:每个周期允许的最大步数。

  • maxStep:允许的最大步数。当迭代次数超过maxStep指定的值时,训练将结束。

  • expRepMaxSize:经验回放的最大大小。经验回放是指基于过去的过渡数量,代理可以决定下一步要采取的行动。

  • doubleDQN:这决定了是否在配置中启用了双重 DQN(如果启用,则为 true)。

  • targetDqnUpdateFreq:常规的 Q 学习在某些条件下可能会高估动作值。双 Q 学习通过增加学习的稳定性来解决这个问题。双 DQN 的主要思想是在每 M 次更新后冻结网络,或者在每 M 次更新后平滑平均。M 的值被称为 targetDqnUpdateFreq

  • updateStart:在开始时进行无操作(什么也不做)的步骤数,以确保 Malmo 任务以随机配置开始。如果代理每次都以相同的方式开始游戏,代理将记住行动序列,而不是根据当前状态学习采取下一个行动。

  • gamma:这也被称为折扣因子。折扣因子会乘以未来的奖励,防止代理被高奖励吸引,而不是学习如何采取行动。接近 1 的折扣因子表示考虑来自远期的奖励,而接近 0 的折扣因子表示考虑来自近期的奖励。

  • rewardFactor:这是一个奖励缩放因子,用于缩放每一步训练的奖励。

  • errorClamp:这会在反向传播期间截断损失函数相对于输出的梯度。对于 errorClamp = 1,梯度分量会被截断到范围 (-1, 1)

  • minEpsilon:Epsilon 是损失函数相对于激活函数输出的导数。每个激活节点的梯度会根据给定的 epsilon 值计算,以用于反向传播。

  • epsilonNbStep:Epsilon 值将在 epsilonNbStep 步骤中退火至 minEpsilon

还有更多...

我们可以通过在代理路径上放置熔岩来让任务变得更加困难,放置熔岩的条件是执行了一定数量的动作。首先,使用 XML 模式创建一个任务规范:

MissionSpec mission = MalmoEnv.loadMissionXML("cliff_walking_rl4j.xml");

现在,设置熔岩挑战任务变得非常简单,如下所示:

mission.drawBlock(xValue, yValue, zValue, "lava");"
 malmoEnv.setMission(mission);

MissionSpec 是一个类文件,包含在 MalmoJavaJar 依赖项中,我们可以用它来设置 Malmo 空间中的任务。

评估 Malmo 代理

我们需要评估代理,看看它在游戏中的表现如何。我们刚刚训练了我们的代理让它在世界中导航并达到目标。在这个过程中,我们将评估训练好的 Malmo 代理。

准备工作

作为前提,我们需要持久化代理的策略,并在评估期间重新加载它们。

代理在训练后使用的最终策略(在 Malmo 空间中进行移动的策略)可以如下面所示保存:

DQNPolicy<MalmoBox> pol = dql.getPolicy();
 pol.save("cliffwalk_pixel.policy");

dql 指的是 DQN 模型。我们获取最终的策略,并将它们存储为 DQNPolicy。DQN 策略提供模型估计的具有最高 Q 值的动作。

它可以在稍后进行恢复以进行评估/推理:

DQNPolicy<MalmoBox> pol = DQNPolicy.load("cliffwalk_pixel.policy");

如何操作...

  1. 创建一个 MDP 封装器来加载任务:
Sample:
 MalmoEnv mdp = new MalmoEnv("cliff_walking_rl4j.xml", actionSpace, observationSpace, obsPolicy);
  1. 评估代理:
Sample:
 double rewards = 0;
 for (int i = 0; i < 10; i++) {
 double reward = pol.play(mdp, new HistoryProcessor(MALMO_HPROC));
 rewards += reward;
 Logger.getAnonymousLogger().info("Reward: " + reward);
 }

它是如何工作的...

Malmo 任务/世界在步骤 1 中启动。在步骤 2 中,MALMO_HPROC 是历史处理器配置。你可以参考之前食谱中的第 6 步,查看示例配置。一旦代理被评估,你应该能看到如下结果:

对于每次任务评估,我们都会计算奖励分数。正向奖励分数表示代理已到达目标。最后,我们会计算代理的平均奖励分数。

在上面的截图中,我们可以看到代理已经到达了目标。这是理想的目标位置,无论代理如何决定在方块中移动。训练结束后,代理将形成最终的策略,代理可以利用该策略到达目标,而不会掉入岩浆。评估过程将确保代理已经足够训练,能够独立进行 Malmo 游戏。

第十章:在分布式环境中开发应用程序

随着数据量和并行计算资源需求的增加,传统方法可能表现不佳。到目前为止,我们已经看到大数据开发因这些原因而变得流行,并成为企业最常采用的方法。DL4J 支持在分布式集群上进行神经网络训练、评估和推理。

现代方法将繁重的训练或输出生成任务分配到多台机器上进行训练。这也带来了额外的挑战。在使用 Spark 执行分布式训练/评估/推理之前,我们需要确保满足以下约束条件:

  • 我们的数据应该足够大,以至于能证明使用分布式集群的必要性。在 Spark 上的小型网络/数据并不会真正带来性能上的提升,在这种情况下,本地机器执行可能会有更好的效果。

  • 我们有多个机器来执行训练/评估或推理。

假设我们有一台配备多个 GPU 处理器的机器。在这种情况下,我们可以简单地使用并行包装器,而不是使用 Spark。并行包装器允许在单台机器上使用多个核心进行并行训练。并行包装器将在第十二章《基准测试和神经网络优化》中讨论,你将在那里了解如何配置它们。此外,如果神经网络每次迭代超过 100 毫秒,可能值得考虑使用分布式训练。

在本章中,我们将讨论如何配置 DL4J 进行分布式训练、评估和推理。我们将为 TinyImageNet 分类器开发一个分布式神经网络。在本章中,我们将覆盖以下内容:

  • 设置 DL4J 和所需的依赖项

  • 为训练创建一个 uber-JAR

  • CPU/GPU 特定的训练配置

  • Spark 的内存设置和垃圾回收

  • 配置编码阈值

  • 执行分布式测试集评估

  • 保存和加载训练好的神经网络模型

  • 执行分布式推理

技术要求

本章的源代码可以在 github.com/PacktPublishing/Java-Deep-Learning-Cookbook/tree/master/10_Developing_applications_in_distributed_environment/sourceCode/cookbookapp/src/main/java/com/javacookbook/app 找到。

克隆我们的 GitHub 仓库后,进入 Java-Deep-Learning-Cookbook/10_Developing_applications_in_distributed_environment/sourceCode 目录。然后,通过导入 pom.xml 文件将 cookbookapp 项目作为 Maven 项目导入。

在运行实际源代码之前,您需要运行以下预处理脚本之一(PreProcessLocal.javaPreProcessSpark.java):

这些脚本可以在cookbookapp项目中找到。

您还需要TinyImageNet数据集,可以在cs231n.stanford.edu/tiny-imagenet-200.zip找到。主页地址为tiny-imagenet.herokuapp.com/

如果您有一些关于使用 Apache Spark 和 Hadoop 的先验知识,那将是非常有益的,这样您能从本章中获得最大的收益。此外,本章假设您的机器已经安装了 Java 并将其添加到环境变量中。我们推荐使用 Java 1.8 版本。

请注意,源代码对硬件(特别是内存/处理能力)有较高要求。我们建议您的主机机器至少拥有 16 GB 的 RAM,特别是在您将源代码运行在笔记本/台式机上时。

设置 DL4J 及其所需依赖项

我们再次讨论如何设置 DL4J,因为我们现在涉及的是一个分布式环境。为了演示目的,我们将使用 Spark 的本地模式。由于此原因,我们可以专注于 DL4J,而不是设置集群、工作节点等。在本示例中,我们将设置一个单节点 Spark 集群(Spark 本地模式),并配置 DL4J 特定的依赖项。

准备工作

为了演示分布式神经网络的使用,您需要以下内容:

  • 分布式文件系统(Hadoop)用于文件管理

  • 分布式计算(Spark)以处理大数据

如何实现...

  1. 添加以下 Maven 依赖项以支持 Apache Spark:
<dependency>
    <groupId>org.apache.spark</groupId>
    <artifactId>spark-core_2.11</artifactId>
    <version>2.1.0</version>
</dependency>
  1. 添加以下 Maven 依赖项以支持 Spark 中的DataVec
<dependency>
    <groupId>org.datavec</groupId>
    <artifactId>datavec-spark_2.11</artifactId>
    <version>1.0.0-beta3_spark_2</version>
</dependency>
  1. 添加以下 Maven 依赖项以支持参数平均:
<dependency>
    <groupId>org.datavec</groupId>
    <artifactId>datavec-spark_2.11</artifactId>
    <version>1.0.0-beta3_spark_2</version>
</dependency>
  1. 添加以下 Maven 依赖项以支持梯度共享:
<dependency>
    <groupId>org.deeplearning4j</groupId>
    <artifactId>dl4j-spark-parameterserver_2.11</artifactId>
    <version>1.0.0-beta3_spark_2</version>
</dependency>
  1. 添加以下 Maven 依赖项以支持 ND4J 后端:
<dependency>
    <groupId>org.nd4j</groupId>
    <artifactId>nd4j-native-platform</artifactId>
    <version>1.0.0-beta3</version>
</dependency>
  1. 添加以下 Maven 依赖项以支持 CUDA:
<dependency>
    <groupId>org.nd4j</groupId>
    <artifactId>nd4j-cuda-x.x</artifactId>
    <version>1.0.0-beta3</version>
</dependency>
  1. 添加以下 Maven 依赖项以支持 JCommander:
<dependency>
    <groupId>com.beust</groupId>
    <artifactId>jcommander</artifactId>
    <version>1.72</version>
</dependency>
  1. 从官方网站hadoop.apache.org/releases.html下载 Hadoop 并添加所需的环境变量。

解压下载的 Hadoop 包并创建以下环境变量:

HADOOP_HOME = {PathDownloaded}/hadoop-x.x 
 HADOOP_HDFS_HOME = {PathDownloaded}/hadoop-x.x 
 HADOOP_MAPRED_HOME = {PathDownloaded}/hadoop-x.x 
 HADOOP_YARN_HOME = {PathDownloaded}/hadoop-x.x 

将以下条目添加到PATH环境变量中:

${HADOOP_HOME}\bin
  1. 为 Hadoop 创建 name/data 节点目录。导航到 Hadoop 主目录(在HADOOP_HOME环境变量中设置),并创建一个名为data的目录。然后,在其下创建名为datanodenamenode的两个子目录。确保已为这些目录提供读/写/删除权限。

  2. 导航到hadoop-x.x/etc/hadoop并打开hdfs-site.xml。然后,添加以下配置:

<configuration>
     <property>
      <name>dfs.replication</name>
      <value>1</value>
     </property>
     <property>
      <name>dfs.namenode.name.dir</name>
      <value>file:/{NameNodeDirectoryPath}</value>
     </property>
     <property>
      <name>dfs.datanode.data.dir</name>
      <value>file:/{DataNodeDirectoryPath}</value>
     </property>
   </configuration>
  1. 导航到hadoop-x.x/etc/hadoop并打开mapred-site.xml。然后,添加以下配置:
<configuration>
  <property>
   <name>mapreduce.framework.name</name>
   <value>yarn</value>
  </property>
 </configuration>
  1. 导航到hadoop-x.x/etc/hadoop并打开yarn-site.xml。然后,添加以下配置:
<configuration>
  <!-- Site specific YARN configuration properties -->
  <property>
   <name>yarn.nodemanager.aux-services</name>
   <value>mapreduce_shuffle</value>
  </property>
  <property>
   <name>yarn.nodemanager.auxservices.mapreduce.shuffle.class</name>
   <value>org.apache.hadoop.mapred.ShuffleHandler</value>
  </property>
 </configuration>
  1. 导航到hadoop-x.x/etc/hadoop并打开core-site.xml。然后,添加以下配置:
<configuration>
  <property>
   <name>fs.default.name</name>
   <value>hdfs://localhost:9000</value>
  </property>
 </configuration> 
  1. 导航到hadoop-x.x/etc/hadoop并打开hadoop-env.cmd。然后,将set JAVA_HOME=%JAVA_HOME%替换为set JAVA_HOME={JavaHomeAbsolutePath}

添加winutils Hadoop 修复(仅适用于 Windows)。你可以从tiny.cc/hadoop-config-windows下载此修复程序。或者,你也可以导航到相关的 GitHub 库github.com/steveloughran/winutils,获取与你安装的 Hadoop 版本匹配的修复程序。将${HADOOP_HOME}中的bin文件夹替换为修复程序中的bin文件夹。

  1. 运行以下 Hadoop 命令来格式化namenode
hdfs namenode –format

你应该看到以下输出:

  1. 导航到${HADOOP_HOME}\sbin并启动 Hadoop 服务:

    • 对于 Windows,运行start-all.cmd

    • 对于 Linux 或任何其他操作系统,从终端运行start-all.sh

你应该看到以下输出:

  1. 在浏览器中访问http://localhost:50070/并验证 Hadoop 是否正常运行:

  1. spark.apache.org/downloads.html下载 Spark 并添加所需的环境变量。解压包并添加以下环境变量:
SPARK_HOME = {PathDownloaded}/spark-x.x-bin-hadoopx.x
SPARK_CONF_DIR = ${SPARK_HOME}\conf
  1. 配置 Spark 的属性。导航到SPARK_CONF_DIR所在的目录,并打开spark-env.sh文件。然后,添加以下配置:
SPARK_MASTER_HOST=localhost

  1. 通过运行以下命令启动 Spark 主节点:
spark-class org.apache.spark.deploy.master.Master

你应该看到以下输出:

  1. 在浏览器中访问http://localhost:8080/并验证 Hadoop 是否正常运行:

它是如何工作的...

在步骤 2 中,为DataVec添加了依赖项。我们需要在 Spark 中使用数据转换函数,就像在常规训练中一样。转换是神经网络的一个数据需求,并非 Spark 特有。

例如,我们在第二章中讨论了LocalTransformExecutor数据提取、转换和加载LocalTransformExecutor用于非分布式环境中的DataVec转换。SparkTransformExecutor将在 Spark 中用于DataVec转换过程。

在步骤 4 中,我们添加了梯度共享的依赖项。梯度共享使得训练时间更快,它被设计为可扩展和容错的。因此,梯度共享优于参数平均。在梯度共享中,不是将所有参数更新/梯度通过网络传递,而是仅更新那些超过指定阈值的部分。假设我们在开始时有一个更新向量,我们希望将其通过网络传递。为了实现这一点,我们将为更新向量中的大值(由阈值指定)创建一个稀疏二进制向量。我们将使用这个稀疏二进制向量进行进一步的通信。主要思路是减少通信工作量。请注意,其余的更新不会被丢弃,而是会被添加到一个残差向量中,稍后处理。残差向量将被保留用于未来的更新(延迟通信),而不会丢失。DL4J 中的梯度共享是异步 SGD 实现。您可以在这里详细阅读:nikkostrom.com/publications/interspeech2015/strom_interspeech2015.pdf

在步骤 5 中,我们为 Spark 分布式训练应用程序添加了 CUDA 依赖项。

下面是关于此项的 uber-JAR 要求:

  • 如果构建 uber-JAR 的操作系统与集群操作系统相同(例如,在 Linux 上运行并在 Spark Linux 集群上执行),请在pom.xml文件中包含nd4j-cuda-x.x依赖项。

  • 如果构建 uber-JAR 的操作系统与集群操作系统不同(例如,在 Windows 上运行并在 Spark Linux 集群上执行),请在pom.xml文件中包含nd4j-cuda-x.x-platform依赖项。

只需将x.x替换为您安装的 CUDA 版本(例如,nd4j-cuda-9.2代表 CUDA 9.2)。

如果集群没有设置 CUDA/cuDNN,可以为集群操作系统包含redist javacpp-预设。您可以参考这里的相应依赖项:deeplearning4j.org/docs/latest/deeplearning4j-config-cuDNN。这样,我们就不需要在每台集群机器上安装 CUDA 或 cuDNN。

在第 6 步中,我们为 JCommander 添加了 Maven 依赖。JCommander 用于解析通过 spark-submit 提供的命令行参数。我们需要这个,因为我们将在 spark-submit 中传递训练/测试数据的目录位置(HDFS/本地)作为命令行参数。

从第 7 步到第 16 步,我们下载并配置了 Hadoop。记得将{PathDownloaded}替换为实际的 Hadoop 包提取位置。同时,将 x.x 替换为你下载的 Hadoop 版本。我们需要指定将存储元数据和数据的磁盘位置,这就是为什么我们在第 8 步/第 9 步创建了 name/data 目录。为了进行修改,在第 10 步中,我们配置了mapred-site.xml。如果你无法在目录中找到该文件,只需通过复制mapred-site.xml.template文件中的所有内容创建一个新的 XML 文件,然后进行第 10 步中提到的修改。

在第 13 步中,我们将 JAVA_HOME 路径变量替换为实际的 Java 主目录位置。这样做是为了避免在运行时遇到某些 ClassNotFound 异常。

在第 18 步中,确保你下载的是与 Hadoop 版本匹配的 Spark 版本。例如,如果你使用 Hadoop 2.7.3,那么下载的 Spark 版本应该是 spark-x.x-bin-hadoop2.7。当我们在第 19 步做出修改时,如果 spark-env.sh 文件不存在,则只需通过复制 spark-env.sh.template 文件中的内容创建一个新文件 spark-env.sh。然后,进行第 19 步中提到的修改。完成此教程中的所有步骤后,你应该能够通过 spark-submit 命令执行分布式神经网络训练。

为训练创建 uber-JAR

通过 spark-submit 执行的训练作业需要在运行时解析所有必需的依赖项。为了管理这个任务,我们将创建一个包含应用程序运行时和其所需依赖项的 uber-JAR。我们将使用 pom.xml 中的 Maven 配置来创建 uber-JAR,这样我们就可以进行分布式训练。实际上,我们将创建一个 uber-JAR,并将其提交到 spark-submit 来执行 Spark 中的训练作业。

在这个教程中,我们将使用 Maven shade 插件为 Spark 训练创建 uber-JAR。

如何操作...

  1. 通过将 Maven shade 插件添加到pom.xml文件中来创建 uber-JAR(阴影 JAR),如下面所示:

有关更多信息,请参考本书 GitHub 仓库中的pom.xml文件:github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/10_Developing%20applications%20in%20distributed%20environment/sourceCode/cookbookapp/pom.xml。将以下过滤器添加到 Maven 配置中:

<filters>
   <filter>
    <artifact>*:*</artifact>
    <excludes>
     <exclude>META-INF/*.SF</exclude>
     <exclude>META-INF/*.DSA</exclude>
     <exclude>META-INF/*.RSA</exclude>
    </excludes>
   </filter>
 </filters>
  1. 执行 Maven 命令以构建项目的 Uber-JAR:
mvn package -DskipTests

它是如何工作的...

在步骤 1 中,您需要指定在执行 JAR 文件时应该运行的主类。在前面的示例中,SparkExample 是我们的主类,用于启动训练会话。您可能会遇到如下异常:

Exception in thread “main” java.lang.SecurityException: Invalid signature file digest for Manifest main attributes.

一些添加到 Maven 配置中的依赖项可能包含签名的 JAR,这可能会导致如下问题。

在步骤 2 中,我们添加了过滤器以防止在 Maven 构建过程中添加签名的 .jars

在步骤 3 中,我们生成了一个包含所有必需依赖项的可执行 .jar 文件。我们可以将此 .jar 文件提交给 spark-submit,在 Spark 上训练我们的网络。该 .jar 文件位于项目的 target 目录中:

Maven Shade 插件不是构建 Uber-JAR 文件的唯一方法。然而,推荐使用 Maven Shade 插件而非其他替代方案。其他替代方案可能无法包含来自源 .jars 的所需文件。其中一些文件作为 Java 服务加载器功能的依赖项。ND4J 利用 Java 的服务加载器功能。因此,其他替代插件可能会导致问题。

训练的 CPU/GPU 特定配置

硬件特定的更改是分布式环境中无法忽视的通用配置。DL4J 支持启用 CUDA/cuDNN 的 NVIDIA GPU 加速训练。我们还可以使用 GPU 执行 Spark 分布式训练。

在这个食谱中,我们将配置 CPU/GPU 特定的更改。

如何操作...

  1. developer.nvidia.com/cuda-downloads 下载、安装并设置 CUDA 工具包。操作系统特定的设置说明可以在 NVIDIA CUDA 官方网站找到。

  2. 通过为 ND4J 的 CUDA 后端添加 Maven 依赖项来配置 Spark 分布式训练的 GPU:

<dependency>
   <groupId>org.nd4j</groupId>
   <artifactId>nd4j-cuda-x.x</artifactId>
   <version>1.0.0-beta3</version>
 </dependency> 
  1. 通过添加 ND4J 本地依赖项来配置 CPU 用于 Spark 分布式训练:
<dependency>
    <groupId>org.nd4j</groupId>
    <artifactId>nd4j-native-platform</artifactId>
    <version>1.0.0-beta3</version>
 </dependency>

它是如何工作的...

我们需要启用一个适当的 ND4J 后端,以便能够利用 GPU 资源,正如我们在步骤 1 中提到的那样。在 pom.xml 文件中启用 nd4j-cuda-x.x 依赖项以进行 GPU 训练,其中 x.x 指您安装的 CUDA 版本。

如果主节点在 CPU 上运行,而工作节点在 GPU 上运行,如前面食谱中所述,我们可以包含两个 ND4J 后端(CUDA / 本地依赖)。如果两个后端都在类路径中,CUDA 后端将首先被尝试。如果因某种原因没有加载,那么将加载 CPU 后端(本地)。通过在主节点中更改 BACKEND_PRIORITY_CPUBACKEND_PRIORITY_GPU 环境变量,也可以更改优先级。后端将根据这些环境变量中的最大值来选择。

在步骤 3 中,我们添加了针对仅有 CPU 硬件的配置。如果主节点/工作节点都配备了 GPU 硬件,那么我们不需要保留此配置。

还有更多内容...

我们可以通过将 cuDNN 配置到 CUDA 设备中来进一步优化训练吞吐量。我们可以在没有安装 CUDA/cuDNN 的情况下,在 Spark 上运行训练实例。为了获得最佳的性能支持,我们可以添加 DL4J CUDA 依赖项。为此,必须添加并使以下组件可用:

  • DL4J CUDA Maven 依赖项:
<dependency>
  <groupId>org.deeplearning4j</groupId>
  <artifactId>deeplearning4j-cuda-x.x</artifactId>
  <version>1.0.0-beta3</version>
 </dependency>

Spark 的内存设置和垃圾回收

内存管理对于大数据集的分布式训练至关重要,尤其是在生产环境中。它直接影响神经网络的资源消耗和性能。内存管理涉及堆内存和堆外内存空间的配置。DL4J/ND4J 特定的内存配置将在 第十二章 中详细讨论,基准测试与神经网络优化

在本教程中,我们将专注于 Spark 环境下的内存配置。

如何操作...

  1. 在提交作业到 spark-submit 时,添加 --executor-memory 命令行参数来设置工作节点的堆内存。例如,我们可以使用 --executor-memory 4g 来分配 4 GB 的内存。

  2. 添加 --conf 命令行参数来设置工作节点的堆外内存:

--conf "spark.executor.extraJavaOptions=-Dorg.bytedeco.javacpp.maxbytes=8G"
  1. 添加 --conf 命令行参数来设置主节点的堆外内存。例如,我们可以使用 --conf "spark.driver.memoryOverhead=-Dorg.bytedeco.javacpp.maxbytes=8G" 来分配 8 GB 的内存。

  2. 添加 --driver-memory 命令行参数来指定主节点的堆内存。例如,我们可以使用 --driver-memory 4g 来分配 4 GB 的内存。

  3. 通过调用 workerTogglePeriodicGC()workerPeriodicGCFrequency() 来为工作节点配置垃圾回收,同时使用 SharedTrainingMaster 设置分布式神经网络:

new SharedTrainingMaster.Builder(voidConfiguration, minibatch)
   .workerTogglePeriodicGC(true) 
   .workerPeriodicGCFrequency(frequencyIntervalInMs) 
   .build();

  1. 通过将以下依赖项添加到 pom.xml 文件中来启用 DL4J 中的 Kryo 优化:
<dependency>
   <groupId>org.nd4j</groupId>
   <artifactId>nd4j-kryo_2.11</artifactId>
  <version>1.0.0-beta3</version>
 </dependency>
  1. 使用 SparkConf 配置 KryoSerializer
SparkConf conf = new SparkConf();
 conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer");
 conf.set("spark.kryo.registrator", "org.nd4j.Nd4jRegistrator");
  1. 如下所示,添加本地性配置到 spark-submit
--conf spark.locality.wait=0 

它是如何工作的...

在步骤 1 中,我们讨论了 Spark 特定的内存配置。我们提到过,这可以为主节点/工作节点进行配置。此外,这些内存配置可能依赖于集群资源管理器。

请注意,--executor-memory 4g命令行参数是针对 YARN 的。请参考相应的集群资源管理器文档,查找以下参数的相应命令行参数:

对于 Spark Standalone,请使用以下命令行选项配置内存空间:

  • 驱动节点的堆内存可以按如下方式配置(8G -> 8GB 内存):
SPARK_DRIVER_MEMORY=8G

  • 驱动节点的非堆内存可以按如下方式配置:
SPARK_DRIVER_OPTS=-Dorg.bytedeco.javacpp.maxbytes=8G

  • 工作节点的堆内存可以按如下方式配置:
SPARK_WORKER_MEMORY=8G

  • 工作节点的非堆内存可以按如下方式配置:
SPARK_WORKER_OPTS=-Dorg.bytedeco.javacpp.maxbytes=8G 

在第 5 步中,我们讨论了工作节点的垃圾回收。一般来说,我们可以通过两种方式控制垃圾回收的频率。以下是第一种方法:

Nd4j.getMemoryManager().setAutoGcWindow(frequencyIntervalInMs);

这将限制垃圾收集器调用的频率为指定的时间间隔,即frequencyIntervalInMs。第二种方法如下:

Nd4j.getMemoryManager().togglePeriodicGc(false);

这将完全禁用垃圾收集器的调用。然而,这些方法不会改变工作节点的内存配置。我们可以使用SharedTrainingMaster中可用的构建器方法来配置工作节点的内存。

我们调用workerTogglePeriodicGC()来禁用/启用周期性垃圾收集器GC)调用,并且调用workerPeriodicGCFrequency()来设置垃圾回收的频率。

在第 6 步中,我们为 ND4J 添加了对 Kryo 序列化的支持。Kryo 序列化器是一个 Java 序列化框架,有助于提高 Spark 训练过程中的速度和效率。

欲了解更多信息,请参考 spark.apache.org/docs/latest/tuning.html。在第 8 步中,本地性配置是一个可选配置,可以用于提高训练性能。数据本地性对 Spark 作业的性能有重大影响。其思路是将数据和代码一起传输,以便计算能够快速执行。欲了解更多信息,请参考 spark.apache.org/docs/latest/tuning.html#data-locality

还有更多内容...

内存配置通常分别应用于主节点/工作节点。因此,仅在工作节点上进行内存配置可能无法达到所需的效果。我们采取的方法可以根据所使用的集群资源管理器而有所不同。因此,参考关于特定集群资源管理器的不同方法的相关文档非常重要。此外,请注意,集群资源管理器中的默认内存设置不适合(过低)那些高度依赖堆外内存空间的库(ND4J/DL4J)。spark-submit 可以通过两种方式加载配置。一种方法是使用 命令行,如前所述,另一种方法是将配置指定在 spark-defaults.conf 文件中,如下所示:

spark.master spark://5.6.7.8:7077
spark.executor.memory 4g

Spark 可以使用 --conf 标志接受任何 Spark 属性。我们在本教程中使用它来指定堆外内存空间。你可以在此处阅读有关 Spark 配置的更多信息:spark.apache.org/docs/latest/configuration.html

  • 数据集应合理分配驱动程序/执行程序的内存。对于 10 MB 的数据,我们不需要为执行程序/驱动程序分配过多内存。在这种情况下,2 GB 至 4 GB 的内存就足够了。分配过多内存不会带来任何区别,反而可能降低性能。

  • 驱动程序是主 Spark 作业运行的进程。 执行器是分配给工作节点的任务,每个任务有其单独的任务。如果应用程序以本地模式运行,则不一定分配驱动程序内存。驱动程序内存连接到主节点,并且与应用程序在 集群 模式下运行时相关。在 集群 模式下,Spark 作业不会在提交的本地机器上运行。Spark 驱动组件将在集群内部启动。

  • Kryo 是一个快速高效的 Java 序列化框架。Kryo 还可以执行对象的自动深拷贝/浅拷贝,以获得高速度、低体积和易于使用的 API。DL4J API 可以利用 Kryo 序列化进一步优化性能。然而,请注意,由于 INDArrays 消耗堆外内存空间,Kryo 可能不会带来太多的性能提升。在使用 Kryo 与 SparkDl4jMultiLayer 或 SparkComputationGraph 类时,请检查相应的日志,以确保 Kryo 配置正确。

  • 就像在常规训练中一样,我们需要为 DL4J Spark 添加合适的 ND4J 后端才能正常运行。对于较新版本的 YARN,可能需要一些额外的配置。有关详细信息,请参考 YARN 文档:hadoop.apache.org/docs/r3.1.0/hadoop-yarn/hadoop-yarn-site/UsingGpus.html

此外,请注意,旧版本(2.7.x 或更早版本)不原生支持 GPU(GPU 和 CPU)。对于这些版本,我们需要使用节点标签来确保作业运行在仅 GPU 的机器上。

  • 如果你进行 Spark 训练,需要注意数据本地性以优化吞吐量。数据本地性确保数据和操作 Spark 作业的代码是一起的,而不是分开的。数据本地性将序列化的代码从一个地方传输到另一个地方(而不是数据块),在那里数据进行操作。它将加速性能,并且不会引入进一步的问题,因为代码的大小远小于数据。Spark 提供了一个名为 spark.locality.wait 的配置属性,用于指定在将数据移至空闲 CPU 之前的超时。如果你将其设置为零,则数据将立即被移动到一个空闲的执行器,而不是等待某个特定的执行器变为空闲。如果空闲执行器距离当前任务执行的执行器较远,那么这将增加额外的工作量。然而,我们通过等待附近的执行器变空闲来节省时间。因此,计算时间仍然可以减少。你可以在 Spark 官方文档中阅读更多关于数据本地性的信息:spark.apache.org/docs/latest/tuning.html#data-locality

配置编码阈值

DL4J Spark 实现利用阈值编码方案跨节点执行参数更新,以减少网络中传输的消息大小,从而降低流量成本。阈值编码方案引入了一个新的分布式训练专用超参数,称为 编码阈值

在这个方案中,我们将在分布式训练实现中配置阈值算法。

如何操作...

  1. SharedTrainingMaster 中配置阈值算法:
TrainingMaster tm = new SharedTrainingMaster.Builder(voidConfiguration, minibatchSize)
   .thresholdAlgorithm(new AdaptiveThresholdAlgorithm(gradientThreshold))
  .build();
  1. 通过调用 residualPostProcessor() 配置残差向量:
TrainingMaster tm = new SharedTrainingMaster.Builder(voidConfiguration, minibatch)
 .residualPostProcessor(new ResidualClippingPostProcessor(clipValue, frequency))
 .build();

它是如何工作的...

在第 1 步中,我们在 SharedTrainingMaster 中配置了阈值算法,默认算法为 AdaptiveThresholdAlgorithm。阈值算法将决定分布式训练的编码阈值,这是一个特定于分布式训练的超参数。此外,值得注意的是,我们并没有丢弃其余的参数更新。如前所述,我们将它们放入单独的残差向量,并在后续处理。这是为了减少训练过程中网络流量/负载。AdaptiveThresholdAlgorithm 在大多数情况下更为优选,以获得更好的性能。

在步骤 2 中,我们使用了ResidualPostProcessor来处理残差向量。残差向量是由梯度共享实现内部创建的,用于收集未被指定边界标记的参数更新。大多数ResidualPostProcessor的实现都会裁剪/衰减残差向量,以确保其中的值不会相对于阈值过大。ResidualClippingPostProcessor就是一种这样的实现。ResidualPostProcessor将防止残差向量变得过大,因为它可能需要太长时间来传输,且可能导致过时的梯度问题。

在步骤 1 中,我们调用了thresholdAlgorithm()来设置阈值算法。在步骤 2 中,我们调用了residualPostProcessor(),以处理用于 DL4J 中的梯度共享实现的残差向量。ResidualClippingPostProcessor接受两个属性:clipValuefrequencyclipValue是我们用于裁剪的当前阈值的倍数。例如,如果阈值为t,而clipValuec,那么残差向量将被裁剪到范围[-c*t , c*t]

还有更多内容…

阈值背后的理念(在我们的上下文中是编码阈值)是,参数更新将在集群间发生,但仅限于那些符合用户定义的限制(阈值)的值。这个阈值值就是我们所说的编码阈值。参数更新指的是在训练过程中梯度值的变化。过高或过低的编码阈值对于获得最佳结果并不理想。因此,提出一个可接受的编码阈值范围是合理的。这也被称为稀疏度比率,其中参数更新发生在集群之间。

在这篇教程中,我们还讨论了如何为分布式训练配置阈值算法。如果AdaptiveThresholdAlgorithm的效果不理想,默认选择是使用它。

以下是 DL4J 中可用的各种阈值算法:

  • AdaptiveThresholdAlgorithm:这是默认的阈值算法,在大多数场景下都能很好地工作。

  • FixedThresholdAlgorithm:这是一种固定且非自适应的阈值策略。

  • TargetSparsityThresholdAlgorithm:这是一种具有特定目标的自适应阈值策略。它通过降低或提高阈值来尝试匹配目标。

执行分布式测试集评估

分布式神经网络训练中存在一些挑战。这些挑战包括管理主节点和工作节点之间的不同硬件依赖关系,配置分布式训练以提高性能,跨分布式集群的内存基准测试等。我们在之前的教程中讨论了一些这些问题。在保持这些配置的同时,我们将继续进行实际的分布式训练/评估。在本教程中,我们将执行以下任务:

  • DL4J Spark 训练的 ETL 过程

  • 为 Spark 训练创建神经网络

  • 执行测试集评估

如何执行...

  1. 下载、解压并将TinyImageNet数据集的内容复制到以下目录位置:
* Windows: C:\Users\<username>\.deeplearning4j\data\TINYIMAGENET_200
 * Linux: ~/.deeplearning4j/data/TINYIMAGENET_200
  1. 使用TinyImageNet数据集创建训练图像批次:
File saveDirTrain = new File(batchSavedLocation, "train");
 SparkDataUtils.createFileBatchesLocal(dirPathDataSet, NativeImageLoader.ALLOWED_FORMATS, true, saveDirTrain, batchSize);
  1. 使用TinyImageNet数据集创建测试图像批次:
File saveDirTest = new File(batchSavedLocation, "test");
 SparkDataUtils.createFileBatchesLocal(dirPathDataSet, NativeImageLoader.ALLOWED_FORMATS, true, saveDirTest, batchSize);
  1. 创建一个ImageRecordReader,它保存数据集的引用:
PathLabelGenerator labelMaker = new ParentPathLabelGenerator();
 ImageRecordReader rr = new ImageRecordReader(imageHeightWidth, imageHeightWidth, imageChannels, labelMaker);
 rr.setLabels(new TinyImageNetDataSetIterator(1).getLabels());
  1. ImageRecordReader创建RecordReaderFileBatchLoader以加载批数据:
RecordReaderFileBatchLoader loader = new RecordReaderFileBatchLoader(rr, batchSize, 1, TinyImageNetFetcher.NUM_LABELS);
 loader.setPreProcessor(new ImagePreProcessingScaler()); 
  1. 在源代码开始时使用 JCommander 解析命令行参数:
JCommander jcmdr = new JCommander(this);
 jcmdr.parse(args);
  1. 使用VoidConfiguration为 Spark 训练创建参数服务器配置(梯度共享),如下代码所示:
VoidConfiguration voidConfiguration = VoidConfiguration.builder()
 .unicastPort(portNumber)
 .networkMask(netWorkMask)
 .controllerAddress(masterNodeIPAddress)
 .build();
  1. 使用SharedTrainingMaster配置分布式训练网络,如下代码所示:
TrainingMaster tm = new SharedTrainingMaster.Builder(voidConfiguration, batchSize)
 .rngSeed(12345)
 .collectTrainingStats(false)
 .batchSizePerWorker(batchSize) // Minibatch size for each worker
 .thresholdAlgorithm(new AdaptiveThresholdAlgorithm(1E-3)) //Threshold algorithm determines the encoding threshold to be use.
 .workersPerNode(1) // Workers per node
 .build();
  1. ComputationGraphConfguration创建GraphBuilder,如下代码所示:
ComputationGraphConfiguration.GraphBuilder builder = new NeuralNetConfiguration.Builder()
 .convolutionMode(ConvolutionMode.Same)
 .l2(1e-4)
 .updater(new AMSGrad(lrSchedule))
 .weightInit(WeightInit.RELU)
 .graphBuilder()
 .addInputs("input")
 .setOutputs("output");

  1. 使用 DL4J 模型库中的DarknetHelper来增强我们的 CNN 架构,如下代码所示:
DarknetHelper.addLayers(builder, 0, 3, 3, 32, 0); //64x64 out
 DarknetHelper.addLayers(builder, 1, 3, 32, 64, 2); //32x32 out
 DarknetHelper.addLayers(builder, 2, 2, 64, 128, 0); //32x32 out
 DarknetHelper.addLayers(builder, 3, 2, 128, 256, 2); //16x16 out
 DarknetHelper.addLayers(builder, 4, 2, 256, 256, 0); //16x16 out
 DarknetHelper.addLayers(builder, 5, 2, 256, 512, 2); //8x8 out
  1. 配置输出层时,考虑标签的数量和损失函数,如下代码所示:
builder.addLayer("convolution2d_6", new ConvolutionLayer.Builder(1, 1)
 .nIn(512)
 .nOut(TinyImageNetFetcher.NUM_LABELS) // number of labels (classified outputs) = 200
 .weightInit(WeightInit.XAVIER)
 .stride(1, 1)
 .activation(Activation.IDENTITY)
 .build(), "maxpooling2d_5")
 .addLayer("globalpooling", new GlobalPoolingLayer.Builder(PoolingType.AVG).build(), "convolution2d_6")
 .addLayer("loss", new LossLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD).activation(Activation.SOFTMAX).build(), "globalpooling")
 .setOutputs("loss");
  1. GraphBuilder创建ComputationGraphConfguration
ComputationGraphConfiguration configuration = builder.build(); 
  1. 从定义的配置创建SparkComputationGraph模型,并为其设置训练监听器:
SparkComputationGraph sparkNet = new SparkComputationGraph(context,configuration,tm);
 sparkNet.setListeners(new PerformanceListener(10, true));
  1. 创建代表我们之前为训练创建的批文件的 HDFS 路径的JavaRDD对象:
String trainPath = dataPath + (dataPath.endsWith("/") ? "" : "/") + "train";
 JavaRDD<String> pathsTrain = SparkUtils.listPaths(context, trainPath);
  1. 通过调用fitPaths()来启动训练实例:
for (int i = 0; i < numEpochs; i++) {
   sparkNet.fitPaths(pathsTrain, loader);
 }

  1. 创建代表我们之前创建的用于测试的批文件的 HDFS 路径的JavaRDD对象:
String testPath = dataPath + (dataPath.endsWith("/") ? "" : "/") + "test";
 JavaRDD<String> pathsTest = SparkUtils.listPaths(context, testPath);
  1. 通过调用doEvaluation()评估分布式神经网络:
Evaluation evaluation = new Evaluation(TinyImageNetDataSetIterator.getLabels(false), 5);
 evaluation = (Evaluation) sparkNet.doEvaluation(pathsTest, loader, evaluation)[0];
 log.info("Evaluation statistics: {}", evaluation.stats());
  1. 在以下格式中通过spark-submit运行分布式训练实例:
spark-submit --master spark://{sparkHostIp}:{sparkHostPort} --class {clssName} {JAR File location absolute path} --dataPath {hdfsPathToPreprocessedData} --masterIP {masterIP}

Example:
 spark-submit --master spark://192.168.99.1:7077 --class com.javacookbook.app.SparkExample cookbookapp-1.0-SNAPSHOT.jar --dataPath hdfs://localhost:9000/user/hadoop/batches/imagenet-preprocessed --masterIP 192.168.99.1

它是如何工作的....

第一步可以通过TinyImageNetFetcher来自动化,如下所示:

TinyImageNetFetcher fetcher = new TinyImageNetFetcher();
 fetcher.downloadAndExtract();

对于任何操作系统,数据需要复制到用户的主目录。一旦执行完毕,我们可以获取训练/测试数据集目录的引用,如下所示:

File baseDirTrain = DL4JResources.getDirectory(ResourceType.DATASET, f.localCacheName() + "/train");
 File baseDirTest = DL4JResources.getDirectory(ResourceType.DATASET, f.localCacheName() + "/test");

你也可以提到自己本地磁盘或 HDFS 的输入目录位置。你需要在第二步中将其作为dirPathDataSet替换。

在第二步和第三步中,我们创建了图像批次,以便优化分布式训练。我们使用了createFileBatchesLocal()来创建这些批次,其中数据来源于本地磁盘。如果你想从 HDFS 源创建批次,则可以使用createFileBatchesSpark()。这些压缩的批文件将节省空间并减少计算瓶颈。假设我们在一个压缩批次中加载了 64 张图像—我们不需要 64 次不同的磁盘读取来处理该批次文件。这些批次包含了多个文件的原始文件内容。

在第 5 步,我们使用RecordReaderFileBatchLoader处理了通过createFileBatchesLocal()createFileBatchesSpark()创建的文件批处理对象。如第 6 步所提到的,你可以使用 JCommander 处理来自spark-submit的命令行参数,或者编写自己的逻辑来处理这些参数。

在第 7 步,我们使用VoidConfiguration类配置了参数服务器。这是一个用于参数服务器的基本配置 POJO 类。我们可以为参数服务器指定端口号、网络掩码等配置。网络掩码在共享网络环境和 YARN 中是非常重要的配置。

在第 8 步,我们开始使用SharedTrainingMaster配置分布式网络进行训练。我们添加了重要的配置,例如阈值算法、工作节点数、最小批量大小等。

从第 9 步和第 10 步开始,我们专注于分布式神经网络层配置。我们使用来自 DL4J 模型库的DarknetHelper,借用了 DarkNet、TinyYOLO 和 YOLO2 的功能。

在第 11 步,我们为我们的微型ImageNet分类器添加了输出层配置。该分类器有 200 个标签,用于进行图像分类预测。在第 13 步,我们使用SparkComputationGraph创建了一个基于 Spark 的ComputationGraph。如果底层网络结构是MultiLayerNetwork,你可以改用SparkDl4jMultiLayer

在第 17 步,我们创建了一个评估实例,如下所示:

Evaluation evaluation = new Evaluation(TinyImageNetDataSetIterator.getLabels(false), 5);

第二个属性(前述代码中的5)表示值N,用于衡量前N的准确性指标。例如,如果true类别的概率是前N个最高的值之一,那么对样本的评估就是正确的。

保存和加载训练好的神经网络模型

反复训练神经网络以进行评估并不是一个好主意,因为训练是一项非常耗费资源的操作。这也是为什么模型持久化在分布式系统中同样重要的原因。

在这个教程中,我们将把分布式神经网络模型持久化到磁盘,并在之后加载以供进一步使用。

如何操作...

  1. 使用ModelSerializer保存分布式神经网络模型:
MultiLayerNetwork model = sparkModel.getNetwork();
 File file = new File("MySparkMultiLayerNetwork.bin");
 ModelSerializer.writeModel(model,file, saveUpdater);
  1. 使用save()保存分布式神经网络模型:
MultiLayerNetwork model = sparkModel.getNetwork();
  File locationToSave = new File("MySparkMultiLayerNetwork.bin);
 model.save(locationToSave, saveUpdater);
  1. 使用ModelSerializer加载分布式神经网络模型:
ModelSerializer.restoreMultiLayerNetwork(new File("MySparkMultiLayerNetwork.bin"));
  1. 使用load()加载分布式神经网络模型:
MultiLayerNetwork restored = MultiLayerNetwork.load(savedModelLocation, saveUpdater);

它是如何工作的...

尽管我们在本地机器上使用save()load()进行模型持久化,但在生产环境中这并不是最佳实践。对于分布式集群环境,我们可以在第 1 步和第 2 步使用BufferedInputStream/BufferedOutputStream将模型保存到集群或从集群加载模型。我们可以像之前展示的那样使用ModelSerializersave()/load()。我们只需注意集群资源管理器和模型持久化,这可以跨集群进行。

还有更多内容...

SparkDl4jMultiLayerSparkComputationGraph内部分别使用了MultiLayerNetworkComputationGraph的标准实现。因此,可以通过调用getNetwork()方法访问它们的内部结构。

执行分布式推理

在本章中,我们讨论了如何使用 DL4J 进行分布式训练。我们还进行了分布式评估,以评估训练好的分布式模型。现在,让我们讨论如何利用分布式模型来解决预测等用例。这被称为推理。接下来,我们将介绍如何在 Spark 环境中进行分布式推理。

在本节中,我们将使用 DL4J 在 Spark 上执行分布式推理。

如何操作...

  1. 通过调用feedForwardWithKey()执行SparkDl4jMultiLayer的分布式推理,如下所示:
SparkDl4jMultiLayer.feedForwardWithKey(JavaPairRDD<K, INDArray> featuresData, int batchSize);
  1. 通过调用feedForwardWithKey()执行SparkComputationGraph的分布式推理:
SparkComputationGraph.feedForwardWithKey(JavaPairRDD<K, INDArray[]> featuresData, int batchSize) ;

工作原理...

第 1 步和第 2 步中feedForwardWithKey()方法的目的是为给定的输入数据集生成输出/预测。该方法返回一个映射。输入数据通过映射中的键表示,结果(输出)通过值(INDArray)表示。

feedForwardWithKey()接受两个参数:输入数据和用于前馈操作的小批量大小。输入数据(特征)采用JavaPairRDD<K, INDArray>的格式。

请注意,RDD 数据是无序的。我们需要一种方法将每个输入映射到相应的结果(输出)。因此,我们需要一个键值对,将每个输入映射到其相应的输出。这就是为什么我们在这里使用键值的主要原因。这与推理过程本身无关。小批量大小的值用于在内存与计算效率之间进行权衡。

第十一章:将迁移学习应用于网络模型

本章将讨论迁移学习方法,它们对于重用先前开发的模型至关重要。我们将展示如何将迁移学习应用于在第三章,构建二分类深度神经网络中创建的模型,以及来自 DL4J 模型库 API 的预训练模型。我们可以使用 DL4J 迁移学习 API 来修改网络架构,在训练过程中保持特定层的参数,并微调模型配置。迁移学习能够提高性能,并且可以开发出更高效的模型。我们将从另一个模型中传递已学习的参数到当前的训练会话。如果你已经为前几章设置好了 DL4J 工作区,那么就不需要在pom.xml中添加新的依赖项;否则,你需要根据第三章,构建二分类深度神经网络中的说明,在pom.xml中添加基本的 Deeplearning4j Maven 依赖项。

本章将涵盖以下内容:

  • 修改现有的客户保持模型

  • 微调学习配置

  • 实现冻结层

  • 导入和加载 Keras 模型及层

技术要求

本章的源代码可以在此找到:github.com/PacktPublishing/Java-Deep-Learning-Cookbook/tree/master/11_Applying_Transfer_Learning_to_network_models/sourceCode/cookbookapp/src/main/java

在克隆 GitHub 仓库后,导航到Java-Deep-Learning-Cookbook/11_Applying_Transfer_Learning_to_network_models/sourceCode目录,然后通过导入pom.xmlcookbookapp项目作为 Maven 项目导入

你需要拥有第三章,构建二分类深度神经网络中的预训练模型,才能运行迁移学习示例。模型文件应该在执行第三章,构建二分类深度神经网络源代码后保存到本地系统中。在执行本章源代码时,你需要在此加载模型。此外,对于SaveFeaturizedDataExample示例,你还需要更新训练/测试目录,以便应用程序能够保存特征化数据集。

修改现有的客户保持模型

我们在第三章中创建了一个客户流失模型,构建二分类深度神经网络,它能够根据指定数据预测客户是否会离开组织。我们可能希望在新的数据上训练现有模型。迁移学习发生在一个现有模型暴露于类似模型的全新训练时。我们使用ModelSerializer类在训练神经网络后保存模型。我们使用前馈网络架构来构建客户保持模型。

在这个实例中,我们将导入一个现有的客户保持模型,并使用 DL4J 迁移学习 API 进一步优化它。

如何做……

  1. 调用load()方法从保存的位置导入模型:
File savedLocation = new File("model.zip");
 boolean saveUpdater = true;
 MultiLayerNetwork restored = MultiLayerNetwork.load(savedLocation, saveUpdater);
  1. 添加所需的pom依赖项以使用deeplearning4j-zoo模块:
<dependency>
 <groupId>org.deeplearning4j</groupId>
 <artifactId>deeplearning4j-zoo</artifactId>
 <version>1.0.0-beta3</version>
 </dependency>
  1. 使用TransferLearning API 为MultiLayerNetwork添加微调配置:
MultiLayerNetwork newModel = new TransferLearning.Builder(oldModel)
 .fineTuneConfiguration(fineTuneConf)
 .build();
  1. 使用TransferLearning API 为ComputationGraph添加微调配置:
ComputationGraph newModel = new TransferLearning.GraphBuilder(oldModel).
 .fineTuneConfiguration(fineTuneConf)
 .build();
  1. 使用TransferLearningHelper配置训练会话。TransferLearningHelper可以通过两种方式创建:

    • 传入使用迁移学习构建器(步骤 2)创建的模型对象,并附加冻结层:
 TransferLearningHelper tHelper = new TransferLearningHelper(newModel);
    • 通过显式指定冻结层,从导入的模型中直接创建:
TransferLearningHelper tHelper = new TransferLearningHelper(oldModel, "layer1")
  1. 使用featurize()方法对训练/测试数据进行特征化:
  while(iterator.hasNext()) {
         DataSet currentFeaturized = transferLearningHelper.featurize(iterator.next());
         saveToDisk(currentFeaturized); //save the featurized date to disk 
        }
  1. 使用ExistingMiniBatchDataSetIterator创建训练/测试迭代器:
DataSetIterator existingTrainingData = new ExistingMiniBatchDataSetIterator(new File("trainFolder"),"churn-"+featureExtractorLayer+"-train-%d.bin");
 DataSetIterator existingTestData = new ExistingMiniBatchDataSetIterator(new File("testFolder"),"churn-"+featureExtractorLayer+"-test-%d.bin");
  1. 通过调用fitFeaturized()在特征化数据上启动训练实例:
 transferLearningHelper.fitFeaturized(existingTrainingData);
  1. 通过调用evaluate()评估未冻结层的模型:
 transferLearningHelper.unfrozenMLN().evaluate(existingTestData);

它是如何工作的……

在步骤 1 中,如果我们计划稍后训练模型,saveUpdater的值将设置为true。我们还讨论了 DL4J 模型库 API 提供的预训练模型。一旦我们按照步骤 1 中提到的添加了deeplearning4j-zoo依赖项,就可以加载如 VGG16 等预训练模型,方法如下:

ZooModel zooModel = VGG16.builder().build();
 ComputationGraph pretrainedNet = (ComputationGraph)    zooModel.initPretrained(PretrainedType.IMAGENET);

DL4J 支持更多在其迁移学习 API 下的预训练模型。

微调配置是将一个训练过的模型调整为执行另一个类似任务的过程。微调配置是迁移学习特有的。在步骤 3 和 4 中,我们为特定类型的神经网络添加了微调配置。以下是使用 DL4J 迁移学习 API 可以进行的可能修改:

  • 更新权重初始化方案、梯度更新策略和优化算法(微调)

  • 修改特定层而不改变其他层

  • 向模型中添加新层

所有这些修改都可以通过迁移学习 API 应用。DL4J 迁移学习 API 提供了一个构建器类来支持这些修改。我们将通过调用fineTuneConfiguration()构建方法来添加微调配置。

正如我们之前所见,在第 4 步中,我们使用GraphBuilder进行基于计算图的迁移学习。请参考我们的 GitHub 仓库以获取具体示例。请注意,迁移学习 API 会在应用所有指定的修改后,从导入的模型返回一个模型实例。常规的Builder类将构建一个MultiLayerNetwork实例,而GraphBuilder则会构建一个ComputationGraph实例。

我们也可能只对某些层进行更改,而不是在所有层之间进行全局更改。主要动机是对那些已识别的层进行进一步优化。这也引出了另一个问题:我们如何知道存储模型的详细信息?为了指定需要保持不变的层,迁移学习 API 要求提供层的属性,如层名/层号。

我们可以使用getLayerWiseConfigurations()方法来获取这些信息,如下所示:

oldModel.getLayerWiseConfigurations().toJson()

执行上述操作后,你应该看到如下所示的网络配置:

完整网络配置的 Gist URL:gist.github.com/rahul-raj/ee71f64706fa47b6518020071711070b

神经网络的配置,如学习率、神经元使用的权重、使用的优化算法、每层特定的配置等,可以从显示的 JSON 内容中验证。

以下是 DL4J 迁移学习 API 支持模型修改的一些可能配置。我们需要层的详细信息(名称/ID)来调用这些方法:

  • setFeatureExtractor(): 用于冻结特定层的变化

  • addLayer(): 用于向模型中添加一个或多个层

  • nInReplace()/nOutReplace(): 通过修改指定层的nInnOut来改变指定层的架构

  • removeLayersFromOutput(): 从模型中删除最后n个层(从需要添加回输出层的点开始)

请注意,导入的迁移学习模型的最后一层是一个全连接层,因为 DL4J 的迁移学习 API 不会强制对导入的模型进行训练配置。所以,我们需要使用addLayer()方法向模型添加输出层。

  • setInputPreProcessor(): 将指定的预处理器添加到指定的层

在第 5 步中,我们看到了在 DL4J 中应用迁移学习的另一种方式,使用TransferLearningHelper。我们讨论了它可以实现的两种方式。当你从迁移学习构建器创建TransferLearningHelper时,你还需要指定FineTuneConfiguration。在FineTuneConfiguration中配置的值将覆盖所有非冻结层的配置。

TransferLearningHelper 与传统迁移学习处理方法的不同之处是有原因的。迁移学习模型通常具有冻结层,这些冻结层在整个训练过程中保持常数值。冻结层的作用取决于对现有模型性能的观察。我们也提到了 setFeatureExtractor() 方法,用于冻结特定的层。使用这个方法可以跳过某些层。然而,模型实例仍然保留整个冻结和非冻结部分。因此,我们在训练期间仍然使用整个模型(包括冻结和非冻结部分)进行计算。

使用 TransferLearningHelper,我们可以通过仅创建非冻结部分的模型实例来减少整体训练时间。冻结的数据集(包括所有冻结参数)将保存到磁盘,我们使用指向非冻结部分的模型实例进行训练。如果我们只需训练一个 epoch,那么 setFeatureExtractor() 和迁移学习助手 API 的性能几乎相同。假设我们有 100 层,其中 99 层是冻结的,并且我们进行 N 次训练。如果我们使用 setFeatureExtractor(),那么我们将为这 99 层做 N 次前向传播,这本质上会增加额外的时间和内存消耗。

为了节省训练时间,我们在使用迁移学习助手 API 保存冻结层的激活结果后创建模型实例。这个过程也被称为特征化。目的是跳过冻结层的计算,并只训练非冻结层。

作为先决条件,需要使用迁移学习构建器定义冻结层,或者在迁移学习助手中明确提到这些冻结层。

TransferLearningHelper 是在步骤 3 中创建的,如下所示:

TransferLearningHelper tHelper = new TransferLearningHelper(oldModel, "layer2")

在前面的例子中,我们明确指定了冻结层的结构,直到 layer2

在步骤 6 中,我们讨论了在特征化后保存数据集。特征化后,我们将数据保存到磁盘。我们将需要获取这些特征化数据以便在其上进行训练。如果将数据集分开并保存到磁盘,训练和评估会变得更加容易。数据集可以使用 save() 方法保存到磁盘,如下所示:

currentFeaturized.save(new File(fileFolder,fileName));

saveTodisk()是保存数据集用于训练或测试的常用方法。实现过程很简单,只需要创建两个不同的目录(train/test),并决定可以用于训练/测试的文件范围。具体实现留给你去做。你可以参考我们的 GitHub 仓库中的示例(SaveFeaturizedDataExample.java):github.com/PacktPublishing/Java-Deep-Learning-Cookbook/blob/master/11_Applying%20Transfer%20Learning%20to%20network%20models/sourceCode/cookbookapp/src/main/java/SaveFeaturizedDataExample.java.

在第 7/8 步中,我们讨论了在特征化数据上训练我们的神经网络。我们的客户保持模型遵循MultiLayerNetwork架构。此训练实例将改变未冻结层的网络配置。因此,我们需要评估未冻结层。在第 5 步中,我们仅对特征化的测试数据进行了模型评估,如下所示:

transferLearningHelper.unfrozenMLN().evaluate(existingTestData);

如果你的网络具有ComputationGraph结构,则可以使用unfrozenGraph()方法来代替unfrozenMLN(),以获得相同的结果。

还有更多...

以下是 DL4J 模型库 API 提供的一些重要的预训练模型:

这是一个非常深的卷积神经网络,旨在解决大规模图像识别任务。我们可以使用迁移学习进一步训练该模型。我们所要做的就是从模型库导入 VGG16:

ZooModel zooModel =VGG16.builder().build();
 ComputationGraph network = (ComputationGraph)zooModel.initPretrained();

请注意,DL4J 模型库 API 中 VGG16 模型的底层架构是ComputationGraph

这是一个实时物体检测模型,用于快速且准确的图像分类。我们同样可以在从模型库导入该模型后应用迁移学习,示例如下:

ComputationGraph pretrained = (ComputationGraph)TinyYOLO.builder().build().initPretrained();

请注意,DL4J 模型库 API 中 TinyYOLO 模型的底层架构是ComputationGraph

这也被称为 YOLOV2,它是一个用于实时物体检测的更快的物体检测模型。我们可以在从模型库导入该模型后,应用迁移学习,示例如下:

 ComputationGraph pretrained = (ComputationGraph) Darknet19.builder().build().initPretrained();

微调学习配置

在执行迁移学习时,我们可能希望更新权重初始化的策略、哪些梯度需要更新、哪些激活函数需要使用等等。为此,我们会对配置进行微调。在本节中,我们将微调迁移学习的配置。

如何做...

  1. 使用FineTuneConfiguration()管理模型配置中的修改:
FineTuneConfiguration fineTuneConf = new FineTuneConfiguration.Builder()
 .optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIENT_DESCENT)
 .updater(new Nesterovs(5e-5))
 .activation(Activation.RELU6)
 .biasInit(0.001)
 .dropOut(0.85)
 .gradientNormalization(GradientNormalization.RenormalizeL2PerLayer)
 .l2(0.0001)
 .weightInit(WeightInit.DISTRIBUTION)
 .seed(seed)
 .build();
  1. 调用fineTuneConfiguration()来微调模型配置:
MultiLayerNetwork newModel = new TransferLearning.Builder(oldModel)
.fineTuneConfiguration(fineTuneConf)
.build();

它是如何工作的...

在第 1 步中我们看到了一个示例的微调实现。微调配置是针对适用于各层的默认/全局更改。因此,如果我们想要从微调配置中排除某些特定层,那么我们需要将这些层冻结。除非我们这么做,否则所有指定修改类型(如梯度、激活等)的当前值将在新模型中被覆盖。

上述所有的微调配置将应用于所有未冻结的层,包括输出层。因此,你可能会遇到由于添加activation()dropOut()方法而产生的错误。Dropout 与隐藏层相关,输出激活可能有不同的值范围。一个快速的解决方法是,除非确实需要,否则删除这些方法。否则,使用迁移学习助手 API 从模型中删除输出层,应用微调,然后用特定的激活函数重新添加输出层。

在第 2 步中,如果我们的原始MultiLayerNetwork模型包含卷积层,那么也可以在卷积模式上进行修改。如你所料,这适用于从第四章进行迁移学习的图像分类模型,构建卷积神经网络。此外,如果你的卷积神经网络需要在支持 CUDA 的 GPU 模式下运行,那么也可以在迁移学习 API 中提到 cuDNN 算法模式。我们可以为 cuDNN 指定一个算法模式(PREFER_FASTESTNO_WORKSPACEUSER_SPECIFIED)。这将影响 cuDNN 的性能和内存使用。使用cudnnAlgoMode()方法并设置PREFER_FASTEST模式可以提升性能。

实现冻结层

我们可能希望将训练实例限制为某些特定的层,这意味着某些层可以保持冻结,以便我们能够集中优化其他层,同时冻结层保持不变。之前我们看过两种实现冻结层的方法:使用常规的迁移学习构建器和使用迁移学习助手。在本例中,我们将为迁移层实现冻结层。

如何操作...

  1. 通过调用setFeatureExtractor()定义冻结层:
MultiLayerNetwork newModel = new TransferLearning.Builder(oldModel)
 .setFeatureExtractor(featurizeExtractionLayer)
 .build();
  1. 调用fit()来启动训练实例:
newModel.fit(numOfEpochs);

它是如何工作的...

在步骤 1 中,我们使用了MultiLayerNetwork进行演示。对于MultiLayerNetworkfeaturizeExtractionLayer指的是层号(整数)。对于ComputationGraphfeaturizeExtractionLayer指的是层名称(String)。通过将冻结层管理移交给迁移学习构建器,它可以与其他所有迁移学习功能(例如微调)一起进行分组,从而实现更好的模块化。然而,迁移学习助手有其自身的优势,正如我们在前面的食谱中讨论的那样。

导入和加载 Keras 模型及层

有时你可能希望导入一个在 DL4J 模型库 API 中不可用的模型。你可能已经在 Keras/TensorFlow 中创建了自己的模型,或者你可能在使用 Keras/TensorFlow 的预训练模型。无论哪种情况,我们仍然可以使用 DL4J 模型导入 API 从 Keras/TensorFlow 加载模型。

准备工作

本食谱假设你已经设置好了 Keras 模型(无论是预训练还是未预训练),并准备将其导入到 DL4J。我们将跳过关于如何将 Keras 模型保存到磁盘的细节,因为它超出了本书的范围。通常,Keras 模型以.h5格式存储,但这并不是限制,因为模型导入 API 也可以导入其他格式。作为前提条件,我们需要在pom.xml中添加以下 Maven 依赖:

<dependency>
   <groupId>org.deeplearning4j</groupId>
   <artifactId>deeplearning4j-modelimport</artifactId>
   <version>1.0.0-beta3</version>
 </dependency>

如何做...

  1. 使用KerasModelImport加载外部MultiLayerNetwork模型:
String modelFileLocation = new ClassPathResource("kerasModel.h5").getFile().getPath();
 MultiLayerNetwork model = KerasModelImport.importKerasSequentialModelAndWeights(modelFileLocation);
  1. 使用KerasModelImport加载外部ComputationGraph模型:
String modelFileLocation = new ClassPathResource("kerasModel.h5").getFile().getPath();
 ComputationGraph model = KerasModelImport.importKerasModelAndWeights(modelFileLocation);
  1. 使用KerasModelBuilder导入外部模型:
KerasModelBuilder builder = new KerasModel().modelBuilder().modelHdf5Filename(modelFile.getAbsolutePath())
 .enforceTrainingConfig(trainConfigToEnforceOrNot);
 if (inputShape != null) {
 builder.inputShape(inputShape);
 }
 KerasModel model = builder.buildModel();
 ComputationGraph newModel = model.getComputationGraph();

它是如何工作的...

在步骤 1 中,我们使用KerasModelImport从磁盘加载外部 Keras 模型。如果模型是通过调用model.to_json()model.save_weights()(在 Keras 中)单独保存的,那么我们需要使用以下变体:

String modelJsonFileLocation = new ClassPathResource("kerasModel.json").getFile().getPath();
 String modelWeightsFileLocation = new ClassPathResource("kerasModelWeights.h5").getFile().getPath();
 MultiLayerNetwork model = KerasModelImport.importKerasSequentialModelAndWeights(modelJsonFileLocation, modelWeightsFileLocation, enforceTrainConfig);

注意以下事项:

  • importKerasSequentialModelAndWeights():从 Keras 模型导入并创建MultiLayerNetwork

  • importKerasModelAndWeights():从 Keras 模型导入并创建ComputationGraph

考虑以下importKerasModelAndWeights()方法实现来执行步骤 2:

KerasModelImport.importKerasModelAndWeights(modelJsonFileLocation,modelWeightsFileLocation,enforceTrainConfig);

第三个属性,enforceTrainConfig,是一个布尔类型,表示是否强制使用训练配置。如果模型是通过调用model.to_json()model.save_weights()(在 Keras 中)单独保存的,那么我们需要使用以下变体:

String modelJsonFileLocation = new ClassPathResource("kerasModel.json").getFile().getPath();
 String modelWeightsFileLocation = new ClassPathResource("kerasModelWeights.h5").getFile().getPath();
 ComputationGraph model = KerasModelImport.importKerasModelAndWeights(modelJsonFileLocation,modelWeightsFileLocation,enforceTrainConfig);

在步骤 3 中,我们讨论了如何使用KerasModelBuilder从外部模型加载ComputationGraph。其中一个构建器方法是inputShape()。它为导入的 Keras 模型指定输入形状。DL4J 要求指定输入形状。然而,如果你选择前面讨论的前两种方法来导入 Keras 模型,你就不需要处理这些问题。那些方法(importKerasModelAndWeights()importKerasSequentialModelAndWeights())在内部使用KerasModelBuilder来导入模型。

第十二章:基准测试与神经网络优化

基准测试是我们用来比较解决方案的标准,以判断它们是否优秀。在深度学习的背景下,我们可能会为表现相当不错的现有模型设定基准。我们可能会根据准确率、处理的数据量、内存消耗和 JVM 垃圾回收调优等因素来测试我们的模型。本章简要讨论了 DL4J 应用程序中的基准测试可能性。我们将从一般指南开始,然后转向更具体的 DL4J 基准测试设置。在本章的最后,我们将介绍一个超参数调优示例,展示如何找到最佳的神经网络参数,以获得最佳的结果。

本章将介绍以下内容:

  • DL4J/ND4J 特定配置

  • 设置堆空间和垃圾回收

  • 使用异步 ETL

  • 使用 arbiter 监控神经网络行为

  • 执行超参数调优

技术要求

本章的代码位于github.com/PacktPublishing/Java-Deep-Learning-Cookbook/tree/master/12_Benchmarking_and_Neural_Network_Optimization/sourceCode/cookbookapp/src/main/java

克隆我们的 GitHub 仓库后,导航到Java-Deep-Learning-Cookbook/12_Benchmarking_and_Neural_Network_Optimization/sourceCode目录。然后通过导入pom.xmlcookbookapp项目作为 Maven 项目导入。

以下是两个示例的链接:

本章的示例基于一个客户流失数据集(github.com/PacktPublishing/Java-Deep-Learning-Cookbook/tree/master/03_Building_Deep_Neural_Networks_for_Binary_classification/sourceCode/cookbookapp/src/main/resources)。该数据集包含在项目目录中。

尽管我们在本章中解释了 DL4J/ND4J 特定的基准测试,但我们建议您遵循一般的基准测试指南。以下是一些常见的神经网络通用基准:

  • 在实际基准任务之前进行预热迭代:预热迭代指的是在开始实际 ETL 操作或网络训练之前,在基准任务上执行的一组迭代。预热迭代非常重要,因为最初的几次执行会很慢。这可能会增加基准任务的总时长,并可能导致错误或不一致的结论。最初几次迭代的缓慢执行可能是由于 JVM 的编译时间,DL4J/ND4J 库的延迟加载方式,或 DL4J/ND4J 库的学习阶段所致。学习阶段是指执行过程中用于学习内存需求的时间。

  • 多次执行基准任务:为了确保基准结果的可靠性,我们需要多次执行基准任务。主机系统可能除了基准实例外还在并行运行多个应用程序/进程。因此,运行时性能会随着时间变化。为了评估这种情况,我们需要多次运行基准任务。

  • 了解基准设置的目的和原因:我们需要评估是否设置了正确的基准。如果我们的目标是操作 a,那么确保只针对操作 a 进行基准测试。同时,我们还必须确保在适当的情况下使用正确的库。始终推荐使用库的最新版本。评估代码中使用的 DL4J/ND4J 配置也非常重要。默认配置在常规情况下可能足够,但为了获得最佳性能,可能需要手动配置。以下是一些默认配置选项,供参考:

    • 内存配置(堆空间设置)。

    • 垃圾回收和工作区配置(更改垃圾回收器调用的频率)。

    • 添加 cuDNN 支持(利用 CUDA 加速的 GPU 机器以获得更好的性能)。

    • 启用 DL4J 缓存模式(为训练实例引入缓存内存)。这将是 DL4J 特定的更改。

我们在第一章中讨论了 cuDNN,Java 中的深度学习介绍,同时谈到了 GPU 环境下的 DL4J。这些配置选项将在接下来的教程中进一步讨论。

  • 在不同规模的任务上运行基准:在多个不同的输入大小/形状上运行基准非常重要,以全面了解其性能。像矩阵乘法这样的数学计算在不同维度下会有所不同。

  • 了解硬件:使用最小批次大小的训练实例在 CPU 上的表现会比在 GPU 系统上更好。当我们使用较大的批次大小时,观察到的情况恰恰相反。此时,训练实例能够利用 GPU 资源。同样,较大的层大小也能更好地利用 GPU 资源。不了解底层硬件就编写网络配置,将无法发挥其全部潜力。

  • 重现基准测试并理解其局限性:为了排查性能瓶颈,我们总是需要重现基准测试。评估性能不佳的情况时,了解其发生的环境非常有帮助。除此之外,我们还需要理解某些基准测试的限制。针对特定层设置的基准测试不会告诉你其他层的性能因素。

  • 避免常见的基准测试错误

    • 考虑使用最新版本的 DL4J/ND4J。为了应用最新的性能改进,可以尝试使用快照版本。

    • 注意使用的本地库类型(例如 cuDNN)。

    • 进行足够多的迭代,并使用合理的批次大小以获得一致的结果。

    • 在对硬件差异未进行考虑的情况下,不要跨硬件进行结果比较。

为了从最新的性能修复中受益,您需要在本地使用最新版本。如果您想在最新修复上运行源代码,并且新版本尚未发布,那么可以使用快照版本。有关如何使用快照版本的详细信息,请访问 deeplearning4j.org/docs/latest/deeplearning4j-config-snapshots

DL4J/ND4J 特定配置

除了常规的基准测试指南外,我们还需要遵循一些特定于 DL4J/ND4J 的附加基准测试配置。这些是针对硬件和数学计算的重要基准测试配置。

由于 ND4J 是 DL4J 的 JVM 计算库,因此基准测试主要针对数学计算。任何关于 ND4J 的基准测试都可以同样应用于 DL4J。让我们来讨论 DL4J/ND4J 特定的基准测试。

准备工作

确保已经从以下链接下载了 cudNN:developer.nvidia.com/cudnn。在尝试将其与 DL4J 配置之前,请先安装它。请注意,cuDNN 并不包含在 CUDA 中,因此仅添加 CUDA 依赖并不足够。

如何操作...

  1. 分离 INDArray 数据以便在不同工作区间使用:
INDArray array = Nd4j.rand(6, 6);
 INDArray mean = array.mean(1);
 INDArray result = mean.detach();
  1. 删除训练/评估过程中创建的所有工作区,以防它们内存不足:
Nd4j.getWorkspaceManager().destroyAllWorkspacesForCurrentThread();
  1. 通过调用 leverageTo() 在当前工作区使用来自其他工作区的数组实例:
LayerWorkspaceMgr.leverageTo(ArrayType.ACTIVATIONS, myArray);
  1. 使用 PerformanceListener 跟踪每次迭代时花费的时间:
model.setListeners(new PerformanceListener(frequency,reportScore)); 
  1. 为支持 cuDNN 添加以下 Maven 依赖:
<dependency>
   <groupId>org.deeplearning4j</groupId>
   <artifactId>deeplearning4j-cuda-x.x</artifactId> //cuda version to be specified
   <version>1.0.0-beta4</version>
 </dependency>
  1. 配置 DL4J/cuDNN 以优先考虑性能而非内存:
MultiLayerNetwork config = new NeuralNetConfiguration.Builder()
 .cudnnAlgoMode(ConvolutionLayer.AlgoMode.PREFER_FASTEST) //prefer performance over memory
 .build();
  1. 配置ParallelWrapper以支持多 GPU 训练/推理:
ParallelWrapper wrapper = new ParallelWrapper.Builder(model)
 .prefetchBuffer(deviceCount)
.workers(Nd4j.getAffinityManager().getNumberOfDevices())
.trainingMode(ParallelWrapper.TrainingMode.SHARED_GRADIENTS)
.thresholdAlgorithm(new AdaptiveThresholdAlgorithm())
 .build();
  1. 按如下方式配置ParallelInference
ParallelInference inference = new ParallelInference.Builder(model)
 .inferenceMode(InferenceMode.BATCHED)
.batchLimit(maxBatchSize)
 .workers(workerCount)
 .build();

它是如何工作的……

工作空间是一种内存管理模型,它使得在无需引入 JVM 垃圾回收器的情况下,实现对循环工作负载的内存重用。每次工作空间循环时,INDArray的内存内容都会失效。工作空间可以用于训练或推理。

在第 1 步中,我们从工作空间基准测试开始。detach()方法将从工作空间中分离出特定的INDArray并返回一个副本。那么,我们如何为训练实例启用工作空间模式呢?如果你使用的是最新的 DL4J 版本(从 1.0.0-alpha 版本起),那么此功能默认已启用。本书中我们使用的目标版本是 1.0.0-beta 3。

在第 2 步中,我们从内存中移除了工作空间,如下所示:

Nd4j.getWorkspaceManager().destroyAllWorkspacesForCurrentThread();

这将仅销毁当前运行线程中的工作空间。通过在相关线程中运行这段代码,我们可以释放工作空间的内存。

DL4J 还允许你为层实现自定义的工作空间管理器。例如,训练期间某一层的激活结果可以放在一个工作空间中,而推理的结果则可以放在另一个工作空间中。这可以通过 DL4J 的LayerWorkspaceMgr来实现,如第 3 步所述。确保返回的数组(第 3 步中的myArray)被定义为ArrayType.ACTIVATIONS

LayerWorkspaceMgr.create(ArrayType.ACTIVATIONS,myArray);

对于训练/推理,使用不同的工作空间模式是可以的。但推荐在训练时使用SEPARATE模式,在推理时使用SINGLE模式,因为推理只涉及前向传播,不涉及反向传播。然而,对于资源消耗/内存较高的训练实例,使用SEPARATE工作空间模式可能更合适,因为它消耗的内存较少。请注意,SEPARATE是 DL4J 中的默认工作空间模式。

在第 4 步中,创建PerformanceListener时使用了两个属性:reportScorefrequencyreportScore是一个布尔变量,frequency是需要追踪时间的迭代次数。如果reportScoretrue,则会报告得分(就像在ScoreIterationListener中一样),并提供每次迭代所花费时间的信息。

在第 7 步中,我们使用了ParallelWrapperParallelInference来支持多 GPU 设备。一旦我们创建了神经网络模型,就可以使用它创建一个并行包装器。我们需要指定设备数量、训练模式以及并行包装器的工作线程数。

我们需要确保训练实例是具备成本效益的。将多个 GPU 添加到系统中并在训练时仅使用一个 GPU 是不现实的。理想情况下,我们希望充分利用所有 GPU 硬件来加速训练/推理过程,并获得更好的结果。ParallelWrapperParallelInference正是为了这个目的。

以下是ParallelWrapperParallelInference支持的一些配置:

  • prefetchBuffer(deviceCount):此并行包装方法指定数据集预取选项。我们在此提到设备的数量。

  • trainingMode(mode):此并行包装方法指定分布式训练方法。SHARED_GRADIENTS指的是分布式训练中的梯度共享方法。

  • workers(Nd4j.getAffinityManager().getNumberOfDevices()):此并行包装方法指定工作者的数量。我们将工作者的数量设置为可用系统的数量。

  • inferenceMode(mode):此并行推理方法指定分布式推理方法。BATCHED模式是一种优化方式。如果大量请求涌入,它会将请求批量处理。如果请求较少,则会按常规处理,不进行批处理。正如你可能猜到的,这是生产环境中的最佳选择。

  • batchLimit(batchSize):此并行推理方法指定批处理大小限制,仅在使用inferenceMode()中的BATCHED模式时适用。

还有更多...

ND4J 操作的性能还可能受到输入数组排序的影响。ND4J 强制执行数组的排序。数学运算(包括一般的 ND4J 操作)的性能取决于输入数组和结果数组的排序。例如,像z = x + y这样的简单加法操作的性能会根据输入数组的排序有所变化。这是因为内存步幅的原因:如果内存序列靠得很近,读取它们会更容易,而不是分布得很远。ND4J 在处理更大的矩阵时运算速度更快。默认情况下,ND4J 数组是 C-顺序的。IC 排序指的是行主序排序,内存分配类似于 C 语言中的数组:

(图片由 Eclipse Deeplearning4j 开发团队提供。Deeplearning4j:用于 JVM 的开源分布式深度学习,Apache 软件基金会许可证 2.0。http://deeplearning4j.org

ND4J 提供了gemm()方法,用于在两个 INDArray 之间进行高级矩阵乘法,具体取决于是否需要在转置后进行乘法运算。此方法返回 F 顺序的结果,这意味着内存分配类似于 Fortran 中的数组。F 顺序指的是列主序排序。假设我们传递了一个 C-顺序的数组来收集gemm()方法的结果;ND4J 会自动检测它,创建一个 F-顺序数组,然后将结果传递给一个 C-顺序数组。

要了解更多关于数组排序以及 ND4J 如何处理数组排序的信息,请访问deeplearning4j.org/docs/latest/nd4j-overview

评估用于训练的迷你批次大小也是至关重要的。我们需要在进行多次训练时,尝试不同的迷你批次大小,并根据硬件规格、数据和评估指标进行调整。在启用 CUDA 的 GPU 环境中,如果使用一个足够大的值,迷你批次大小将在基准测试中起到重要作用。当我们谈论一个大的迷你批次大小时,我们是指可以根据整个数据集来合理化的迷你批次大小。对于非常小的迷你批次大小,我们在基准测试后不会观察到 CPU/GPU 有明显的性能差异。与此同时,我们还需要关注模型准确度的变化。理想的迷你批次大小是当我们充分利用硬件性能的同时,不影响模型准确度。事实上,我们的目标是在更好的性能(更短的训练时间)下获得更好的结果。

设置堆空间和垃圾回收

内存堆空间和垃圾回收是经常被讨论的话题,但却往往是最常被忽略的基准测试。在使用 DL4J/ND4J 时,你可以配置两种类型的内存限制:堆内存和非堆内存。每当 JVM 垃圾回收器回收一个INDArray时,非堆内存将被释放,前提是它不在其他地方使用。在本教程中,我们将设置堆空间和垃圾回收以进行基准测试。

如何操作...

  1. 向 Eclipse/IntelliJ IDE 中添加所需的 VM 参数,如以下示例所示:
-Xms1G -Xmx6G -Dorg.bytedeco.javacpp.maxbytes=16G -Dorg.bytedeco.javacpp.maxphysicalbytes=20G

例如,在 IntelliJ IDE 中,我们可以将 VM 参数添加到运行时配置中:

  1. 在更改内存限制以适应硬件后,运行以下命令(用于命令行执行):
java -Xms1G -Xmx6G -Dorg.bytedeco.javacpp.maxbytes=16G -Dorg.bytedeco.javacpp.maxphysicalbytes=20G YourClassName

  1. 配置 JVM 的服务器风格代际垃圾回收器:
java -XX:+UseG1GC
  1. 使用 ND4J 减少垃圾回收器调用的频率:
Nd4j.getMemoryManager().setAutoGcWindow(3000);
  1. 禁用垃圾回收器调用,而不是执行第 4 步:
Nd4j.getMemoryManager().togglePeriodicGc(false);
  1. 在内存映射文件中分配内存块,而不是使用 RAM:
WorkspaceConfiguration memoryMap = WorkspaceConfiguration.builder()
 .initialSize(2000000000)
 .policyLocation(LocationPolicy.MMAP)
 .build();
 try (MemoryWorkspace workspace = Nd4j.getWorkspaceManager().getAndActivateWorkspace(memoryMap, "M")) {
 INDArray example = Nd4j.create(10000);
 }

它是如何工作的...

在第 1 步中,我们进行了堆内存/非堆内存配置。堆内存指的是由 JVM 堆(垃圾回收器)管理的内存。非堆内存则是指不被直接管理的内存,例如 INDArrays 使用的内存。通过以下 Java 命令行选项,我们可以控制堆内存和非堆内存的限制:

  • -Xms:此选项定义了应用启动时 JVM 堆将消耗的内存量。

  • -Xmx:此选项定义了 JVM 堆在运行时可以消耗的最大内存。它仅在需要时分配内存,且不会超过此限制。

  • -Dorg.bytedeco.javacpp.maxbytes:此选项指定非堆内存的限制。

  • -Dorg.bytedeco.javacpp.maxphysicalbytes:此选项指定可以分配给应用程序的最大字节数。通常,这个值比-Xmxmaxbytes的组合值要大。

假设我们想要在堆内最初配置 1 GB,在堆内最大配置 6 GB,在堆外配置 16 GB,并在进程的最大内存为 20 GB,VM 参数将如下所示,并如步骤 1 所示:

-Xms1G -Xmx6G -Dorg.bytedeco.javacpp.maxbytes=16G -Dorg.bytedeco.javacpp.maxphysicalbytes=20G

请注意,您需要根据硬件可用内存进行相应调整。

还可以将这些 VM 选项设置为环境变量。我们可以创建一个名为MAVEN_OPTS的环境变量并将 VM 选项放置在其中。您可以选择步骤 1 或步骤 2,或者设置环境变量。完成此操作后,可以跳转到步骤 3。

在步骤 3、4 和 5 中,我们讨论了通过一些垃圾收集优化自动管理内存。垃圾收集器管理内存管理并消耗堆内内存。DL4J 与垃圾收集器紧密耦合。如果我们谈论 ETL,每个DataSetIterator对象占用 8 字节内存。垃圾收集器可能会进一步增加系统的延迟。为此,我们在步骤 3 中配置了G1GC(即Garbage First Garbage Collector)调优。

如果我们将 0 毫秒(毫秒)作为属性传递给setAutoGcWindow()方法(如步骤 4 所示),它将只是禁用此特定选项。getMemoryManager()将返回一个用于更低级别内存管理的后端特定实现的MemoryManager

在步骤 6 中,我们讨论了配置内存映射文件以为INDArrays分配更多内存。我们在步骤 4 中创建了一个 1 GB 的内存映射文件。请注意,只有使用nd4j-native库时才能创建和支持内存映射文件。内存映射文件比 RAM 中的内存分配速度慢。如果小批量大小的内存需求高于可用 RAM 量,则可以应用步骤 4。

这还不是全部……

DL4J 与 JavaCPP 有依赖关系,后者充当 Java 和 C++之间的桥梁:github.com/bytedeco/javacpp

JavaCPP 基于堆空间(堆外内存)上设置的-Xmx值运行。DL4J 寻求垃圾收集器和 JavaCPP 的帮助来释放内存。

对于涉及大量数据的训练会话,重要的是为堆外内存空间(JVM)提供比堆内内存更多的 RAM。为什么?因为我们的数据集和计算涉及到INDArrays,并存储在堆外内存空间中。

识别运行应用程序的内存限制非常重要。以下是需要正确配置内存限制的一些情况:

  • 对于 GPU 系统,maxbytesmaxphysicalbytes是重要的内存限制设置。这里我们处理的是堆外内存。为这些设置分配合理的内存允许我们使用更多的 GPU 资源。

  • 对于涉及内存分配问题的 RunTimeException,一个可能的原因是堆外内存空间不可用。如果我们没有使用 设置堆空间和垃圾回收 章节中讨论的内存限制(堆外内存空间)设置,堆外内存空间可能会被 JVM 垃圾回收器回收,从而导致内存分配问题。

  • 如果你的环境内存有限,建议不要为 -Xmx-Xms 选项设置过大的值。例如,如果我们为 8 GB 内存的系统使用 -Xms6G,那么仅剩下 2 GB 的内存空间用于堆外内存、操作系统和其他进程。

另见

使用异步 ETL

我们使用同步 ETL 进行演示。但在生产环境中,推荐使用异步 ETL。在生产环境中,一个低性能的 ETA 组件可能会导致性能瓶颈。在 DL4J 中,我们使用 DataSetIterator 将数据加载到磁盘。它可以从磁盘、内存中加载数据,或者简单地异步加载数据。异步 ETL 在后台使用异步加载器。通过多线程,它将数据加载到 GPU/CPU 中,其他线程负责计算任务。在下面的操作步骤中,我们将在 DL4J 中执行异步 ETL 操作。

如何操作...

  1. 使用异步预取创建异步迭代器:
DatasetIterator asyncIterator = new AsyncMultiDataSetIterator(iterator);
  1. 使用同步预取创建异步迭代器:
DataSetIterator shieldIterator = new AsyncShieldDataSetIterator(iterator);

它是如何工作的...

在第一步中,我们使用 AsyncMultiDataSetIterator 创建了一个迭代器。我们可以使用 AsyncMultiDataSetIteratorAsyncDataSetIterator 来创建异步迭代器。AsyncMultiDataSetIterator 有多种配置方式。你可以通过传递其他属性来创建 AsyncMultiDataSetIterator,例如 queSize(一次可以预取的迷你批次的数量)和 useWorkSpace(布尔类型,表示是否应该使用工作区配置)。在使用 AsyncDataSetIterator 时,我们会在调用 next() 获取下一个数据集之前使用当前数据集。还需要注意的是,在没有调用 detach() 的情况下不应存储数据集。如果这样做,数据集中 INDArray 数据使用的内存最终会在 AsyncDataSetIterator 中被覆盖。对于自定义迭代器实现,确保你在训练/评估过程中不要通过 next() 调用初始化大型对象。相反,应将所有初始化工作放在构造函数内,以避免不必要的工作区内存消耗。

在步骤 2 中,我们使用AsyncShieldDataSetIterator创建了一个迭代器。要选择退出异步预取,我们可以使用AsyncShieldMultiDataSetIteratorAsyncShieldDataSetIterator。这些包装器将在数据密集型操作(如训练)中防止异步预取,可以用于调试目的。

如果训练实例每次运行时都执行 ETL 操作,实际上我们每次都在重新创建数据。最终,整个过程(训练和评估)会变得更慢。我们可以通过使用预先保存的数据集来更好地处理这一点。我们在上一章中讨论了使用ExistingMiniBatchDataSetIterator进行预保存,当时我们预保存了特征数据,并随后使用ExistingMiniBatchDataSetIterator加载它。我们可以将其转换为异步迭代器(如步骤 1 或步骤 2 所示),一举两得:使用异步加载的预保存数据。这本质上是一个性能基准,进一步优化了 ETL 过程。

还有更多...

假设我们的迷你批次有 100 个样本,并且我们将queSize设置为10;每次将预取 1,000 个样本。工作区的内存需求取决于数据集的大小,这来自于底层的迭代器。工作区将根据不同的内存需求进行调整(例如,长度变化的时间序列)。请注意,异步迭代器是通过LinkedBlockingQueue在内部支持的。这个队列数据结构以先进先出FIFO)模式对元素进行排序。在并发环境中,链式队列通常比基于数组的队列有更高的吞吐量。

使用 arbiter 监控神经网络行为

超参数优化/调优是寻找学习过程中超参数的最优值的过程。超参数优化部分自动化了使用某些搜索策略来寻找最佳超参数的过程。Arbiter 是 DL4J 深度学习库的一部分,用于超参数优化。Arbiter 可以通过调整神经网络的超参数来找到高性能的模型。Arbiter 有一个用户界面,用于可视化超参数调优过程的结果。

在这个配方中,我们将设置 arbiter 并可视化训练实例,观察神经网络的行为。

如何操作...

  1. pom.xml中添加 arbiter Maven 依赖:
<dependency>
   <groupId>org.deeplearning4j</groupId>
   <artifactId>arbiter-deeplearning4j</artifactId>
   <version>1.0.0-beta3</version>
 </dependency>
 <dependency>
   <groupId>org.deeplearning4j</groupId>
   <artifactId>arbiter-ui_2.11</artifactId>
   <version>1.0.0-beta3</version>
 </dependency>
  1. 使用ContinuousParameterSpace配置搜索空间:
ParameterSpace<Double> learningRateParam = new ContinuousParameterSpace(0.0001,0.01);
  1. 使用IntegerParameterSpace配置搜索空间:
ParameterSpace<Integer> layerSizeParam = new IntegerParameterSpace(5,11);   
  1. 使用OptimizationConfiguration来结合执行超参数调优过程所需的所有组件:
OptimizationConfiguration optimizationConfiguration = new             OptimizationConfiguration.Builder()
 .candidateGenerator(candidateGenerator)
 .dataProvider(dataProvider)
 .modelSaver(modelSaver)
 .scoreFunction(scoreFunction)
 .terminationConditions(conditions)
 .build();

它是如何工作的...

在步骤 2 中,我们创建了ContinuousParameterSpace来配置超参数优化的搜索空间:

ParameterSpace<Double> learningRateParam = new ContinuousParameterSpace(0.0001,0.01);

在前述情况下,超参数调优过程将选择学习率在(0.0001, 0.01)范围内的连续值。请注意,仲裁者并不会自动化超参数调优过程。我们仍然需要指定值的范围或选项列表,以便超参数调优过程进行。换句话说,我们需要指定一个搜索空间,其中包含所有有效的值,供调优过程选择最佳组合,从而获得最佳结果。我们还提到了IntegerParameterSpace,它的搜索空间是一个整数的有序空间,位于最大/最小值之间。

由于有多个不同配置的训练实例,因此超参数优化调优过程需要一段时间才能完成。最后,将返回最佳配置。

在步骤 2 中,一旦我们使用ParameterSpaceOptimizationConfiguration定义了搜索空间,我们需要将其添加到MultiLayerSpaceComputationGraphSpace中。这些是 DL4J 的MultiLayerConfigurationComputationGraphConfiguration的仲裁者对应物。

然后,我们使用candidateGenerator()构建方法添加了candidateGeneratorcandidateGenerator为超参数调优选择候选者(各种超参数组合)。它可以使用不同的方法,如随机搜索和网格搜索,来选择下一个用于超参数调优的配置。

scoreFunction()指定在超参数调优过程中用于评估的评估指标。

terminationConditions()用于指定所有的训练终止条件。超参数调优随后将进行下一个配置。

执行超参数调优

一旦使用ParameterSpaceOptimizationConfiguration定义了搜索空间,并且有可能的值范围,下一步是使用MultiLayerSpaceComputationGraphSpace完成网络配置。之后,我们开始训练过程。在超参数调优过程中,我们会执行多个训练会话。

在这个示例中,我们将执行并可视化超参数调优过程。我们将在演示中使用MultiLayerSpace

如何实现...

  1. 使用IntegerParameterSpace为层大小添加搜索空间:
ParameterSpace<Integer> layerSizeParam = new IntegerParameterSpace(startLimit,endLimit);
  1. 使用ContinuousParameterSpace为学习率添加搜索空间:
ParameterSpace<Double> learningRateParam = new ContinuousParameterSpace(0.0001,0.01);
  1. 使用MultiLayerSpace通过将所有搜索空间添加到相关的网络配置中来构建配置空间:
MultiLayerSpace hyperParamaterSpace = new MultiLayerSpace.Builder()
 .updater(new AdamSpace(learningRateParam))
 .addLayer(new DenseLayerSpace.Builder()
   .activation(Activation.RELU)
   .nIn(11)
   .nOut(layerSizeParam)
   .build())
 .addLayer(new DenseLayerSpace.Builder()
   .activation(Activation.RELU)
   .nIn(layerSizeParam)
   .nOut(layerSizeParam)
   .build())
 .addLayer(new OutputLayerSpace.Builder()
   .activation(Activation.SIGMOID)
   .lossFunction(LossFunctions.LossFunction.XENT)
   .nOut(1)
   .build())
 .build();

  1. MultiLayerSpace创建candidateGenerator
Map<String,Object> dataParams = new HashMap<>();
 dataParams.put("batchSize",new Integer(10));

CandidateGenerator candidateGenerator = new RandomSearchGenerator(hyperParamaterSpace,dataParams);
  1. 通过实现DataSource接口来创建数据源:
public static class ExampleDataSource implements DataSource{
  public ExampleDataSource(){
     //implement methods from DataSource
  }
 }

我们需要实现四个方法:configure()trainData()testData()getDataType()

    • 以下是configure()的示例实现:
public void configure(Properties properties) {
    this.minibatchSize = Integer.parseInt(properties.getProperty("minibatchSize", "16"));
 }
    • 这是getDataType()的示例实现:
public Class<?> getDataType() {
 return DataSetIterator.class;
 }
    • 这是trainData()的示例实现:
public Object trainData() {
 try{
 DataSetIterator iterator = new RecordReaderDataSetIterator(dataPreprocess(),minibatchSize,labelIndex,numClasses);
 return dataSplit(iterator).getTestIterator();
 }
 catch(Exception e){
 throw new RuntimeException();
 }
 }
    • 这是testData()的示例实现:
public Object testData() {
 try{
 DataSetIterator iterator = new RecordReaderDataSetIterator(dataPreprocess(),minibatchSize,labelIndex,numClasses);
 return dataSplit(iterator).getTestIterator();
 }
 catch(Exception e){
 throw new RuntimeException();
 }
 }
  1. 创建一个终止条件数组:
TerminationCondition[] conditions = {
   new MaxTimeCondition(maxTimeOutInMinutes, TimeUnit.MINUTES),
   new MaxCandidatesCondition(maxCandidateCount)
};
  1. 计算使用不同配置组合创建的所有模型的得分:
ScoreFunction scoreFunction = new EvaluationScoreFunction(Evaluation.Metric.ACCURACY);
  1. 创建OptimizationConfiguration并添加终止条件和评分函数:
OptimizationConfiguration optimizationConfiguration = new OptimizationConfiguration.Builder()
 .candidateGenerator(candidateGenerator)
 .dataSource(ExampleDataSource.class,dataSourceProperties)
 .modelSaver(modelSaver)
 .scoreFunction(scoreFunction)
 .terminationConditions(conditions)
 .build();
  1. 创建LocalOptimizationRunner以运行超参数调优过程:
IOptimizationRunner runner = new LocalOptimizationRunner(optimizationConfiguration,new MultiLayerNetworkTaskCreator());
  1. LocalOptimizationRunner添加监听器,以确保事件正确记录(跳到第 11 步添加ArbiterStatusListener):
runner.addListeners(new LoggingStatusListener());
  1. 通过调用execute()方法执行超参数调优:
runner.execute();
  1. 存储模型配置并将LoggingStatusListener替换为ArbiterStatusListener
StatsStorage storage = new FileStatsStorage(new File("HyperParamOptimizationStatsModel.dl4j"));
 runner.addListeners(new ArbiterStatusListener(storage));
  1. 将存储附加到UIServer
UIServer.getInstance().attach(storage);
  1. 运行超参数调优会话,并访问以下 URL 查看可视化效果:
http://localhost:9000/arbiter
  1. 评估超参数调优会话中的最佳得分,并在控制台中显示结果:
double bestScore = runner.bestScore();
 int bestCandidateIndex = runner.bestScoreCandidateIndex();
 int numberOfConfigsEvaluated = runner.numCandidatesCompleted();

你应该会看到以下快照中显示的输出。显示了模型的最佳得分、最佳模型所在的索引以及在过程中过滤的配置数量:

它是如何工作的...

在第 4 步中,我们设置了一种策略,通过该策略从搜索空间中选择网络配置。我们为此目的使用了CandidateGenerator。我们创建了一个参数映射来存储所有数据映射,以便与数据源一起使用,并将其传递给CandidateGenerator

在第 5 步中,我们实现了configure()方法以及来自DataSource接口的另外三个方法。configure()方法接受一个Properties属性,其中包含所有要与数据源一起使用的参数。如果我们想传递miniBatchSize作为属性,则可以创建一个Properties实例,如下所示:

Properties dataSourceProperties = new Properties();
 dataSourceProperties.setProperty("minibatchSize", "64");

请注意,迷你批量大小需要作为字符串 "64" 提供,而不是 64

自定义的dataPreprocess()方法对数据进行预处理。dataSplit()创建DataSetIteratorSplitter来生成训练/评估的迭代器。

在第 4 步中,RandomSearchGenerator通过随机方式生成超参数调优的候选项。如果我们明确提到超参数的概率分布,那么随机搜索将根据其概率偏向这些超参数。GridSearchCandidateGenerator通过网格搜索生成候选项。对于离散型超参数,网格大小等于超参数值的数量。对于整数型超参数,网格大小与min(discretizationCount,max-min+1)相同。

在第 6 步中,我们定义了终止条件。终止条件控制训练过程的进展程度。终止条件可以是MaxTimeConditionMaxCandidatesCondition,或者我们可以定义自己的终止条件。

在第 7 步中,我们创建了一个评分函数,用于说明在超参数优化过程中如何评估每个模型。

在第 8 步中,我们创建了包含这些终止条件的 OptimizationConfiguration。除了终止条件外,我们还向 OptimizationConfiguration 添加了以下配置:

  • 模型信息需要存储的位置

  • 之前创建的候选生成器

  • 之前创建的数据源

  • 要考虑的评估指标类型

OptimizationConfiguration 将所有组件结合起来执行超参数优化。请注意,dataSource() 方法需要两个属性:一个是数据源类的类类型,另一个是我们想要传递的数据源属性(在我们的示例中是 minibatchSize)。modelSaver() 构建方法要求你指定正在训练的模型的存储位置。我们可以将模型信息(模型评分及其他配置)存储在资源文件夹中,然后创建一个 ModelSaver 实例,如下所示:

ResultSaver modelSaver = new FileModelSaver("resources/");

为了使用裁判进行可视化,跳过第 10 步,按照第 12 步操作,然后执行可视化任务运行器。

在遵循第 13 和第 14 步的指示之后,你应该能够看到裁判的 UI 可视化,如下所示:

从裁判可视化中找出最佳模型评分非常直观且容易。如果你运行了多个超参数调优的会话,你可以从顶部的下拉列表中选择特定的会话。此时,UI 上显示的其他重要信息也非常易于理解。

posted @ 2025-07-13 15:43  绝不原创的飞龙  阅读(7)  评论(0)    收藏  举报