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。

深度学习的直觉

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/aff78d29-b0a5-4b31-984b-c329227ed7da.png

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/78a06015-1bc7-42a6-bf4c-df580e370f0a.png

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

反向传播

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/fc9dffda-d154-47bc-852c-25b7fca57cb9.png

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/d882cc72-71ce-454a-9289-1cb7b3261bbf.png

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

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/be8a3dd1-e5e9-4241-ab1c-f036d08f883a.png

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

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/0061cfc1-2139-4c7a-bbd5-612b220c2b28.png

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/f553df10-3d7d-4339-9a60-14d4afe47f67.png

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

还有更多…

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

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

循环神经网络可能最适合处理序列数据。你可以添加 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 条记录进行了以下训练:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/8669f24e-f834-47fa-9a11-2b6e4e8e85fa.png

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/4dc5d658-38f8-4eda-9402-b2531fbcbeab.png

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/4b100a4e-a906-4e13-a870-639b4447828f.png

您可以观察到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 网站:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/9472152e-f538-4507-8961-48716ebdd996.png

请注意,步骤 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 的示例:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/128dd62a-9167-4658-9060-b6a46a826555.png

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

还有更多…

除了前面讨论的常见运行时问题,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会递归地查找指定目录位置中的文件。你还可以传递一个字符串数组作为参数,用来表示允许的扩展名:

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/9d46081e-a227-496a-b7a1-0a42637fd175.png

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/1e782ca9-2cda-437e-a4ef-8e439d440b05.png

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

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/0c1f8ed8-7480-4aca-b2ae-bf2acc32172f.png

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/8de37b76-2610-4b79-b76f-c5c30b1c21fc.png

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

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

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

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/7615f0ca-4974-470f-a2e6-04b1ac481993.png

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

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/9b26e276-e149-4fc0-ad68-2d53fe43a441.png

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/c44c2da0-a1dc-4526-851e-53c06731b8eb.png

执行第 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 格式):

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/239553a8-700f-42fe-9403-1f0b2ecbb7b0.png

请注意,在应用归一化时,不能跳过第 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 个特征。每一行代表一个客户/记录,如下图所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/9530d8be-7666-495f-aafc-18676893e788.png

我们的数据集是一个包含 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 个特征:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/5dee1e86-791a-445c-9737-deaf4dca7527.png

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/40ca2a93-1976-4b5f-93b3-d99d95d8c993.png

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

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

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

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/621c3773-9d43-498e-967a-efe1e0b9a104.png

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

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

还有更多内容…

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/9107f6f7-a7c4-4830-9296-a068771ee108.png

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/8a761959-3786-4d70-a828-462ebf049e10.png

上面两个截图显示的是通过调用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 将在数据提取/加载时抛出运行时错误。

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/73912ea2-ea7a-4786-ab3b-953c6202b743.png

在前面的截图中,运行时异常是由于 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特征的最终模式将如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/7be56c83-76d3-420b-9f58-f2a314822f5b.png

在第 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 个输入特征。输入层的输出神经元没有特定要求。如果我们为输入层分配错误数量的输入神经元,可能会导致运行时错误:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/c3ddc74c-a677-46b4-a5e1-c95d2beebdc4.png

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 获取训练数据集的迭代器,并在其上训练我们的模型。如果你正确配置了日志记录器,你应该能够看到训练实例正在按以下方式进展:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/5ecf204a-abe2-4e43-b91b-322a0cc52540.png

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

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/17bebe0a-7847-4d9a-9bd1-2b548b788b06.png

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

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/4c1035e0-e297-43a7-a888-ab4c9e8c935e.png

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

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/77ed7dd4-5797-4cae-b87c-fd26f1bf72a2.png

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/5a7cc605-7c53-45af-8f80-4cb47ba58eec.png

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/832323a4-3b9d-4ac1-bcdf-b02d2aaccb33.png

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

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/67b76595-73b8-49a8-81b5-14b956783fda.png

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

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

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

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

还有更多内容…

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

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

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

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

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

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/7069f9da-c4bb-47a3-a704-6266367bfe9b.png

请注意,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文件:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/6b90ef87-fda2-44ee-a975-7dfe1effeb85.png

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/48d532f3-0671-4d86-b898-2595035b3407.png

  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

预测结果将如下所示:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/0ebc2020-8ab6-4743-baab-b22d16011c10.png

它是如何工作的…

我们需要创建一个 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://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/770b3c00-d390-4d41-9b67-8cd238a57618.jpg

(图像来源: 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()); 

评估指标将显示如下:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/ba5b0d89-60a4-4939-b38f-579bc1a573c0.png

  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%的丢弃率,超过此范围可能会导致欠拟合。没有一个固定的常数值适用于所有情况,因为它会根据具体情况而有所不同:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/059cd7f6-e130-4234-97bb-71913a34d140.png

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 并点击“创建应用程序”按钮:

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/03aa855c-b3a4-4bdc-9027-aad8828684f2.png

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/cf2b0a73-1c7b-4628-96c3-1c20f1cd9d75.png

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/91c64ce6-3d2b-498b-9c14-3cb8453e924d.png

现在,你的应用程序引擎已经在 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 分布式环境中。

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

https://github.com/OpenDocCN/freelearn-dl-pt6-zh/raw/master/docs/java-dl-cb/img/fa8bb5bd-d9d9-43ff-ae39-b882886b7cf4.png

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值