Java 深度学习秘籍(三)
原文:
annas-archive.org/md5/f5d28e569048a2e5329b5ab42aa19b12译者:飞龙
第十章:在分布式环境中开发应用程序
随着数据量和并行计算资源需求的增加,传统方法可能表现不佳。到目前为止,我们已经看到大数据开发因这些原因而变得流行,并成为企业最常采用的方法。DL4J 支持在分布式集群上进行神经网络训练、评估和推理。
现代方法将繁重的训练或输出生成任务分配到多台机器上进行训练。这也带来了额外的挑战。在使用 Spark 执行分布式训练/评估/推理之前,我们需要确保满足以下约束条件:
-
我们的数据应该足够大,以至于能证明使用分布式集群的必要性。在 Spark 上的小型网络/数据并不会真正带来性能上的提升,在这种情况下,本地机器执行可能会有更好的效果。
-
我们有多个机器来执行训练/评估或推理。
假设我们有一台配备多个 GPU 处理器的机器。在这种情况下,我们可以简单地使用并行包装器,而不是使用 Spark。并行包装器允许在单台机器上使用多个核心进行并行训练。并行包装器将在第十二章《基准测试和神经网络优化》中讨论,你将在那里了解如何配置它们。此外,如果神经网络每次迭代超过 100 毫秒,可能值得考虑使用分布式训练。
在本章中,我们将讨论如何配置 DL4J 进行分布式训练、评估和推理。我们将为 TinyImageNet 分类器开发一个分布式神经网络。在本章中,我们将覆盖以下内容:
-
设置 DL4J 和所需的依赖项
-
为训练创建一个 uber-JAR
-
CPU/GPU 特定的训练配置
-
Spark 的内存设置和垃圾回收
-
配置编码阈值
-
执行分布式测试集评估
-
保存和加载训练好的神经网络模型
-
执行分布式推理
技术要求
克隆我们的 GitHub 仓库后,进入 Java-Deep-Learning-Cookbook/10_Developing_applications_in_distributed_environment/sourceCode 目录。然后,通过导入 pom.xml 文件将 cookbookapp 项目作为 Maven 项目导入。
在运行实际源代码之前,您需要运行以下预处理脚本之一(PreProcessLocal.java或PreProcessSpark.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)以处理大数据
如何实现...
- 添加以下 Maven 依赖项以支持 Apache Spark:
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-core_2.11</artifactId>
<version>2.1.0</version>
</dependency>
- 添加以下 Maven 依赖项以支持 Spark 中的
DataVec:
<dependency>
<groupId>org.datavec</groupId>
<artifactId>datavec-spark_2.11</artifactId>
<version>1.0.0-beta3_spark_2</version>
</dependency>
- 添加以下 Maven 依赖项以支持参数平均:
<dependency>
<groupId>org.datavec</groupId>
<artifactId>datavec-spark_2.11</artifactId>
<version>1.0.0-beta3_spark_2</version>
</dependency>
- 添加以下 Maven 依赖项以支持梯度共享:
<dependency>
<groupId>org.deeplearning4j</groupId>
<artifactId>dl4j-spark-parameterserver_2.11</artifactId>
<version>1.0.0-beta3_spark_2</version>
</dependency>
- 添加以下 Maven 依赖项以支持 ND4J 后端:
<dependency>
<groupId>org.nd4j</groupId>
<artifactId>nd4j-native-platform</artifactId>
<version>1.0.0-beta3</version>
</dependency>
- 添加以下 Maven 依赖项以支持 CUDA:
<dependency>
<groupId>org.nd4j</groupId>
<artifactId>nd4j-cuda-x.x</artifactId>
<version>1.0.0-beta3</version>
</dependency>
- 添加以下 Maven 依赖项以支持 JCommander:
<dependency>
<groupId>com.beust</groupId>
<artifactId>jcommander</artifactId>
<version>1.72</version>
</dependency>
- 从官方网站
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
-
为 Hadoop 创建 name/data 节点目录。导航到 Hadoop 主目录(在
HADOOP_HOME环境变量中设置),并创建一个名为data的目录。然后,在其下创建名为datanode和namenode的两个子目录。确保已为这些目录提供读/写/删除权限。 -
导航到
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>
- 导航到
hadoop-x.x/etc/hadoop并打开mapred-site.xml。然后,添加以下配置:
<configuration>
<property>
<name>mapreduce.framework.name</name>
<value>yarn</value>
</property>
</configuration>
- 导航到
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>
- 导航到
hadoop-x.x/etc/hadoop并打开core-site.xml。然后,添加以下配置:
<configuration>
<property>
<name>fs.default.name</name>
<value>hdfs://localhost:9000</value>
</property>
</configuration>
- 导航到
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文件夹。
- 运行以下 Hadoop 命令来格式化
namenode:
hdfs namenode –format
你应该看到以下输出:
-
导航到
${HADOOP_HOME}\sbin并启动 Hadoop 服务:-
对于 Windows,运行
start-all.cmd。 -
对于 Linux 或任何其他操作系统,从终端运行
start-all.sh。
-
你应该看到以下输出:
- 在浏览器中访问
http://localhost:50070/并验证 Hadoop 是否正常运行:
- 从
spark.apache.org/downloads.html下载 Spark 并添加所需的环境变量。解压包并添加以下环境变量:
SPARK_HOME = {PathDownloaded}/spark-x.x-bin-hadoopx.x
SPARK_CONF_DIR = ${SPARK_HOME}\conf
- 配置 Spark 的属性。导航到
SPARK_CONF_DIR所在的目录,并打开spark-env.sh文件。然后,添加以下配置:
SPARK_MASTER_HOST=localhost
- 通过运行以下命令启动 Spark 主节点:
spark-class org.apache.spark.deploy.master.Master
你应该看到以下输出:
- 在浏览器中访问
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。
如何操作...
- 通过将 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>
- 执行 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 特定的更改。
如何操作...
-
从
developer.nvidia.com/cuda-downloads下载、安装并设置 CUDA 工具包。操作系统特定的设置说明可以在 NVIDIA CUDA 官方网站找到。 -
通过为 ND4J 的 CUDA 后端添加 Maven 依赖项来配置 Spark 分布式训练的 GPU:
<dependency>
<groupId>org.nd4j</groupId>
<artifactId>nd4j-cuda-x.x</artifactId>
<version>1.0.0-beta3</version>
</dependency>
- 通过添加 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_CPU 和 BACKEND_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>
- cuDNN 库文件在
developer.nvidia.com/cuDNN。请注意,你需要注册 NVIDIA 网站账户才能下载 cuDNN 库。注册是免费的。请参阅安装指南:docs.nvidia.com/deeplearning/sdk/cuDNN-install/index.html。
Spark 的内存设置和垃圾回收
内存管理对于大数据集的分布式训练至关重要,尤其是在生产环境中。它直接影响神经网络的资源消耗和性能。内存管理涉及堆内存和堆外内存空间的配置。DL4J/ND4J 特定的内存配置将在 第十二章 中详细讨论,基准测试与神经网络优化。
在本教程中,我们将专注于 Spark 环境下的内存配置。
如何操作...
-
在提交作业到
spark-submit时,添加--executor-memory命令行参数来设置工作节点的堆内存。例如,我们可以使用--executor-memory 4g来分配 4 GB 的内存。 -
添加
--conf命令行参数来设置工作节点的堆外内存:
--conf "spark.executor.extraJavaOptions=-Dorg.bytedeco.javacpp.maxbytes=8G"
-
添加
--conf命令行参数来设置主节点的堆外内存。例如,我们可以使用--conf "spark.driver.memoryOverhead=-Dorg.bytedeco.javacpp.maxbytes=8G"来分配 8 GB 的内存。 -
添加
--driver-memory命令行参数来指定主节点的堆内存。例如,我们可以使用--driver-memory 4g来分配 4 GB 的内存。 -
通过调用
workerTogglePeriodicGC()和workerPeriodicGCFrequency()来为工作节点配置垃圾回收,同时使用SharedTrainingMaster设置分布式神经网络:
new SharedTrainingMaster.Builder(voidConfiguration, minibatch)
.workerTogglePeriodicGC(true)
.workerPeriodicGCFrequency(frequencyIntervalInMs)
.build();
- 通过将以下依赖项添加到
pom.xml文件中来启用 DL4J 中的 Kryo 优化:
<dependency>
<groupId>org.nd4j</groupId>
<artifactId>nd4j-kryo_2.11</artifactId>
<version>1.0.0-beta3</version>
</dependency>
- 使用
SparkConf配置KryoSerializer:
SparkConf conf = new SparkConf();
conf.set("spark.serializer", "org.apache.spark.serializer.KryoSerializer");
conf.set("spark.kryo.registrator", "org.nd4j.Nd4jRegistrator");
- 如下所示,添加本地性配置到
spark-submit:
--conf spark.locality.wait=0
它是如何工作的...
在步骤 1 中,我们讨论了 Spark 特定的内存配置。我们提到过,这可以为主节点/工作节点进行配置。此外,这些内存配置可能依赖于集群资源管理器。
请注意,--executor-memory 4g命令行参数是针对 YARN 的。请参考相应的集群资源管理器文档,查找以下参数的相应命令行参数:
-
Spark Standalone:
spark.apache.org/docs/latest/spark-standalone.html
对于 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 实现利用阈值编码方案跨节点执行参数更新,以减少网络中传输的消息大小,从而降低流量成本。阈值编码方案引入了一个新的分布式训练专用超参数,称为 编码阈值。
在这个方案中,我们将在分布式训练实现中配置阈值算法。
如何操作...
- 在
SharedTrainingMaster中配置阈值算法:
TrainingMaster tm = new SharedTrainingMaster.Builder(voidConfiguration, minibatchSize)
.thresholdAlgorithm(new AdaptiveThresholdAlgorithm(gradientThreshold))
.build();
- 通过调用
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接受两个属性:clipValue和frequency。clipValue是我们用于裁剪的当前阈值的倍数。例如,如果阈值为t,而clipValue为c,那么残差向量将被裁剪到范围**[-c*t , c*t]**。
还有更多内容…
阈值背后的理念(在我们的上下文中是编码阈值)是,参数更新将在集群间发生,但仅限于那些符合用户定义的限制(阈值)的值。这个阈值值就是我们所说的编码阈值。参数更新指的是在训练过程中梯度值的变化。过高或过低的编码阈值对于获得最佳结果并不理想。因此,提出一个可接受的编码阈值范围是合理的。这也被称为稀疏度比率,其中参数更新发生在集群之间。
在这篇教程中,我们还讨论了如何为分布式训练配置阈值算法。如果AdaptiveThresholdAlgorithm的效果不理想,默认选择是使用它。
以下是 DL4J 中可用的各种阈值算法:
-
AdaptiveThresholdAlgorithm:这是默认的阈值算法,在大多数场景下都能很好地工作。 -
FixedThresholdAlgorithm:这是一种固定且非自适应的阈值策略。 -
TargetSparsityThresholdAlgorithm:这是一种具有特定目标的自适应阈值策略。它通过降低或提高阈值来尝试匹配目标。
执行分布式测试集评估
分布式神经网络训练中存在一些挑战。这些挑战包括管理主节点和工作节点之间的不同硬件依赖关系,配置分布式训练以提高性能,跨分布式集群的内存基准测试等。我们在之前的教程中讨论了一些这些问题。在保持这些配置的同时,我们将继续进行实际的分布式训练/评估。在本教程中,我们将执行以下任务:
-
DL4J Spark 训练的 ETL 过程
-
为 Spark 训练创建神经网络
-
执行测试集评估
如何执行...
- 下载、解压并将
TinyImageNet数据集的内容复制到以下目录位置:
* Windows: C:\Users\<username>\.deeplearning4j\data\TINYIMAGENET_200
* Linux: ~/.deeplearning4j/data/TINYIMAGENET_200
- 使用
TinyImageNet数据集创建训练图像批次:
File saveDirTrain = new File(batchSavedLocation, "train");
SparkDataUtils.createFileBatchesLocal(dirPathDataSet, NativeImageLoader.ALLOWED_FORMATS, true, saveDirTrain, batchSize);
- 使用
TinyImageNet数据集创建测试图像批次:
File saveDirTest = new File(batchSavedLocation, "test");
SparkDataUtils.createFileBatchesLocal(dirPathDataSet, NativeImageLoader.ALLOWED_FORMATS, true, saveDirTest, batchSize);
- 创建一个
ImageRecordReader,它保存数据集的引用:
PathLabelGenerator labelMaker = new ParentPathLabelGenerator();
ImageRecordReader rr = new ImageRecordReader(imageHeightWidth, imageHeightWidth, imageChannels, labelMaker);
rr.setLabels(new TinyImageNetDataSetIterator(1).getLabels());
- 从
ImageRecordReader创建RecordReaderFileBatchLoader以加载批数据:
RecordReaderFileBatchLoader loader = new RecordReaderFileBatchLoader(rr, batchSize, 1, TinyImageNetFetcher.NUM_LABELS);
loader.setPreProcessor(new ImagePreProcessingScaler());
- 在源代码开始时使用 JCommander 解析命令行参数:
JCommander jcmdr = new JCommander(this);
jcmdr.parse(args);
- 使用
VoidConfiguration为 Spark 训练创建参数服务器配置(梯度共享),如下代码所示:
VoidConfiguration voidConfiguration = VoidConfiguration.builder()
.unicastPort(portNumber)
.networkMask(netWorkMask)
.controllerAddress(masterNodeIPAddress)
.build();
- 使用
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();
- 为
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");
- 使用 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
- 配置输出层时,考虑标签的数量和损失函数,如下代码所示:
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");
- 从
GraphBuilder创建ComputationGraphConfguration:
ComputationGraphConfiguration configuration = builder.build();
- 从定义的配置创建
SparkComputationGraph模型,并为其设置训练监听器:
SparkComputationGraph sparkNet = new SparkComputationGraph(context,configuration,tm);
sparkNet.setListeners(new PerformanceListener(10, true));
- 创建代表我们之前为训练创建的批文件的 HDFS 路径的
JavaRDD对象:
String trainPath = dataPath + (dataPath.endsWith("/") ? "" : "/") + "train";
JavaRDD<String> pathsTrain = SparkUtils.listPaths(context, trainPath);
- 通过调用
fitPaths()来启动训练实例:
for (int i = 0; i < numEpochs; i++) {
sparkNet.fitPaths(pathsTrain, loader);
}
- 创建代表我们之前创建的用于测试的批文件的 HDFS 路径的
JavaRDD对象:
String testPath = dataPath + (dataPath.endsWith("/") ? "" : "/") + "test";
JavaRDD<String> pathsTest = SparkUtils.listPaths(context, testPath);
- 通过调用
doEvaluation()评估分布式神经网络:
Evaluation evaluation = new Evaluation(TinyImageNetDataSetIterator.getLabels(false), 5);
evaluation = (Evaluation) sparkNet.doEvaluation(pathsTest, loader, evaluation)[0];
log.info("Evaluation statistics: {}", evaluation.stats());
- 在以下格式中通过
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个最高的值之一,那么对样本的评估就是正确的。
保存和加载训练好的神经网络模型
反复训练神经网络以进行评估并不是一个好主意,因为训练是一项非常耗费资源的操作。这也是为什么模型持久化在分布式系统中同样重要的原因。
在这个教程中,我们将把分布式神经网络模型持久化到磁盘,并在之后加载以供进一步使用。
如何操作...
- 使用
ModelSerializer保存分布式神经网络模型:
MultiLayerNetwork model = sparkModel.getNetwork();
File file = new File("MySparkMultiLayerNetwork.bin");
ModelSerializer.writeModel(model,file, saveUpdater);
- 使用
save()保存分布式神经网络模型:
MultiLayerNetwork model = sparkModel.getNetwork();
File locationToSave = new File("MySparkMultiLayerNetwork.bin);
model.save(locationToSave, saveUpdater);
- 使用
ModelSerializer加载分布式神经网络模型:
ModelSerializer.restoreMultiLayerNetwork(new File("MySparkMultiLayerNetwork.bin"));
- 使用
load()加载分布式神经网络模型:
MultiLayerNetwork restored = MultiLayerNetwork.load(savedModelLocation, saveUpdater);
它是如何工作的...
尽管我们在本地机器上使用save()或load()进行模型持久化,但在生产环境中这并不是最佳实践。对于分布式集群环境,我们可以在第 1 步和第 2 步使用BufferedInputStream/BufferedOutputStream将模型保存到集群或从集群加载模型。我们可以像之前展示的那样使用ModelSerializer或save()/load()。我们只需注意集群资源管理器和模型持久化,这可以跨集群进行。
还有更多内容...
SparkDl4jMultiLayer和SparkComputationGraph内部分别使用了MultiLayerNetwork和ComputationGraph的标准实现。因此,可以通过调用getNetwork()方法访问它们的内部结构。
执行分布式推理
在本章中,我们讨论了如何使用 DL4J 进行分布式训练。我们还进行了分布式评估,以评估训练好的分布式模型。现在,让我们讨论如何利用分布式模型来解决预测等用例。这被称为推理。接下来,我们将介绍如何在 Spark 环境中进行分布式推理。
在本节中,我们将使用 DL4J 在 Spark 上执行分布式推理。
如何操作...
- 通过调用
feedForwardWithKey()执行SparkDl4jMultiLayer的分布式推理,如下所示:
SparkDl4jMultiLayer.feedForwardWithKey(JavaPairRDD<K, INDArray> featuresData, int batchSize);
- 通过调用
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 仓库后,导航到Java-Deep-Learning-Cookbook/11_Applying_Transfer_Learning_to_network_models/sourceCode目录,然后通过导入pom.xml将cookbookapp项目作为 Maven 项目导入。
你需要拥有第三章,构建二分类深度神经网络中的预训练模型,才能运行迁移学习示例。模型文件应该在执行第三章,构建二分类深度神经网络源代码后保存到本地系统中。在执行本章源代码时,你需要在此加载模型。此外,对于SaveFeaturizedDataExample示例,你还需要更新训练/测试目录,以便应用程序能够保存特征化数据集。
修改现有的客户保持模型
我们在第三章中创建了一个客户流失模型,构建二分类深度神经网络,它能够根据指定数据预测客户是否会离开组织。我们可能希望在新的数据上训练现有模型。迁移学习发生在一个现有模型暴露于类似模型的全新训练时。我们使用ModelSerializer类在训练神经网络后保存模型。我们使用前馈网络架构来构建客户保持模型。
在这个实例中,我们将导入一个现有的客户保持模型,并使用 DL4J 迁移学习 API 进一步优化它。
如何做……
- 调用
load()方法从保存的位置导入模型:
File savedLocation = new File("model.zip");
boolean saveUpdater = true;
MultiLayerNetwork restored = MultiLayerNetwork.load(savedLocation, saveUpdater);
- 添加所需的
pom依赖项以使用deeplearning4j-zoo模块:
<dependency>
<groupId>org.deeplearning4j</groupId>
<artifactId>deeplearning4j-zoo</artifactId>
<version>1.0.0-beta3</version>
</dependency>
- 使用
TransferLearningAPI 为MultiLayerNetwork添加微调配置:
MultiLayerNetwork newModel = new TransferLearning.Builder(oldModel)
.fineTuneConfiguration(fineTuneConf)
.build();
- 使用
TransferLearningAPI 为ComputationGraph添加微调配置:
ComputationGraph newModel = new TransferLearning.GraphBuilder(oldModel).
.fineTuneConfiguration(fineTuneConf)
.build();
-
使用
TransferLearningHelper配置训练会话。TransferLearningHelper可以通过两种方式创建:- 传入使用迁移学习构建器(步骤 2)创建的模型对象,并附加冻结层:
TransferLearningHelper tHelper = new TransferLearningHelper(newModel);
-
- 通过显式指定冻结层,从导入的模型中直接创建:
TransferLearningHelper tHelper = new TransferLearningHelper(oldModel, "layer1")
- 使用
featurize()方法对训练/测试数据进行特征化:
while(iterator.hasNext()) {
DataSet currentFeaturized = transferLearningHelper.featurize(iterator.next());
saveToDisk(currentFeaturized); //save the featurized date to disk
}
- 使用
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");
- 通过调用
fitFeaturized()在特征化数据上启动训练实例:
transferLearningHelper.fitFeaturized(existingTrainingData);
- 通过调用
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(): 通过修改指定层的nIn或nOut来改变指定层的架构 -
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:文中提到的 VGG-16:
arxiv.org/abs/1409.1556。
这是一个非常深的卷积神经网络,旨在解决大规模图像识别任务。我们可以使用迁移学习进一步训练该模型。我们所要做的就是从模型库导入 VGG16:
ZooModel zooModel =VGG16.builder().build();
ComputationGraph network = (ComputationGraph)zooModel.initPretrained();
请注意,DL4J 模型库 API 中 VGG16 模型的底层架构是ComputationGraph。
- TinyYOLO:文中提到的 TinyYOLO:
arxiv.org/pdf/1612.08242.pdf。
这是一个实时物体检测模型,用于快速且准确的图像分类。我们同样可以在从模型库导入该模型后应用迁移学习,示例如下:
ComputationGraph pretrained = (ComputationGraph)TinyYOLO.builder().build().initPretrained();
请注意,DL4J 模型库 API 中 TinyYOLO 模型的底层架构是ComputationGraph。
- Darknet19:文中提到的 Darknet19:
arxiv.org/pdf/1612.08242.pdf。
这也被称为 YOLOV2,它是一个用于实时物体检测的更快的物体检测模型。我们可以在从模型库导入该模型后,应用迁移学习,示例如下:
ComputationGraph pretrained = (ComputationGraph) Darknet19.builder().build().initPretrained();
微调学习配置
在执行迁移学习时,我们可能希望更新权重初始化的策略、哪些梯度需要更新、哪些激活函数需要使用等等。为此,我们会对配置进行微调。在本节中,我们将微调迁移学习的配置。
如何做...
- 使用
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();
- 调用
fineTuneConfiguration()来微调模型配置:
MultiLayerNetwork newModel = new TransferLearning.Builder(oldModel)
.fineTuneConfiguration(fineTuneConf)
.build();
它是如何工作的...
在第 1 步中我们看到了一个示例的微调实现。微调配置是针对适用于各层的默认/全局更改。因此,如果我们想要从微调配置中排除某些特定层,那么我们需要将这些层冻结。除非我们这么做,否则所有指定修改类型(如梯度、激活等)的当前值将在新模型中被覆盖。
上述所有的微调配置将应用于所有未冻结的层,包括输出层。因此,你可能会遇到由于添加activation()和dropOut()方法而产生的错误。Dropout 与隐藏层相关,输出激活可能有不同的值范围。一个快速的解决方法是,除非确实需要,否则删除这些方法。否则,使用迁移学习助手 API 从模型中删除输出层,应用微调,然后用特定的激活函数重新添加输出层。
在第 2 步中,如果我们的原始MultiLayerNetwork模型包含卷积层,那么也可以在卷积模式上进行修改。如你所料,这适用于从第四章进行迁移学习的图像分类模型,构建卷积神经网络。此外,如果你的卷积神经网络需要在支持 CUDA 的 GPU 模式下运行,那么也可以在迁移学习 API 中提到 cuDNN 算法模式。我们可以为 cuDNN 指定一个算法模式(PREFER_FASTEST、NO_WORKSPACE 或 USER_SPECIFIED)。这将影响 cuDNN 的性能和内存使用。使用cudnnAlgoMode()方法并设置PREFER_FASTEST模式可以提升性能。
实现冻结层
我们可能希望将训练实例限制为某些特定的层,这意味着某些层可以保持冻结,以便我们能够集中优化其他层,同时冻结层保持不变。之前我们看过两种实现冻结层的方法:使用常规的迁移学习构建器和使用迁移学习助手。在本例中,我们将为迁移层实现冻结层。
如何操作...
- 通过调用
setFeatureExtractor()定义冻结层:
MultiLayerNetwork newModel = new TransferLearning.Builder(oldModel)
.setFeatureExtractor(featurizeExtractionLayer)
.build();
- 调用
fit()来启动训练实例:
newModel.fit(numOfEpochs);
它是如何工作的...
在步骤 1 中,我们使用了MultiLayerNetwork进行演示。对于MultiLayerNetwork,featurizeExtractionLayer指的是层号(整数)。对于ComputationGraph,featurizeExtractionLayer指的是层名称(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>
如何做...
- 使用
KerasModelImport加载外部MultiLayerNetwork模型:
String modelFileLocation = new ClassPathResource("kerasModel.h5").getFile().getPath();
MultiLayerNetwork model = KerasModelImport.importKerasSequentialModelAndWeights(modelFileLocation);
- 使用
KerasModelImport加载外部ComputationGraph模型:
String modelFileLocation = new ClassPathResource("kerasModel.h5").getFile().getPath();
ComputationGraph model = KerasModelImport.importKerasModelAndWeights(modelFileLocation);
- 使用
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 仓库后,导航到Java-Deep-Learning-Cookbook/12_Benchmarking_and_Neural_Network_Optimization/sourceCode目录。然后通过导入pom.xml将cookbookapp项目作为 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 依赖并不足够。
如何操作...
- 分离
INDArray数据以便在不同工作区间使用:
INDArray array = Nd4j.rand(6, 6);
INDArray mean = array.mean(1);
INDArray result = mean.detach();
- 删除训练/评估过程中创建的所有工作区,以防它们内存不足:
Nd4j.getWorkspaceManager().destroyAllWorkspacesForCurrentThread();
- 通过调用
leverageTo()在当前工作区使用来自其他工作区的数组实例:
LayerWorkspaceMgr.leverageTo(ArrayType.ACTIVATIONS, myArray);
- 使用
PerformanceListener跟踪每次迭代时花费的时间:
model.setListeners(new PerformanceListener(frequency,reportScore));
- 为支持 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>
- 配置 DL4J/cuDNN 以优先考虑性能而非内存:
MultiLayerNetwork config = new NeuralNetConfiguration.Builder()
.cudnnAlgoMode(ConvolutionLayer.AlgoMode.PREFER_FASTEST) //prefer performance over memory
.build();
- 配置
ParallelWrapper以支持多 GPU 训练/推理:
ParallelWrapper wrapper = new ParallelWrapper.Builder(model)
.prefetchBuffer(deviceCount)
.workers(Nd4j.getAffinityManager().getNumberOfDevices())
.trainingMode(ParallelWrapper.TrainingMode.SHARED_GRADIENTS)
.thresholdAlgorithm(new AdaptiveThresholdAlgorithm())
.build();
- 按如下方式配置
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时使用了两个属性:reportScore和frequency。reportScore是一个布尔变量,frequency是需要追踪时间的迭代次数。如果reportScore为true,则会报告得分(就像在ScoreIterationListener中一样),并提供每次迭代所花费时间的信息。
在第 7 步中,我们使用了ParallelWrapper或ParallelInference来支持多 GPU 设备。一旦我们创建了神经网络模型,就可以使用它创建一个并行包装器。我们需要指定设备数量、训练模式以及并行包装器的工作线程数。
我们需要确保训练实例是具备成本效益的。将多个 GPU 添加到系统中并在训练时仅使用一个 GPU 是不现实的。理想情况下,我们希望充分利用所有 GPU 硬件来加速训练/推理过程,并获得更好的结果。ParallelWrapper和ParallelInference正是为了这个目的。
以下是ParallelWrapper和ParallelInference支持的一些配置:
-
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。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时,非堆内存将被释放,前提是它不在其他地方使用。在本教程中,我们将设置堆空间和垃圾回收以进行基准测试。
如何操作...
- 向 Eclipse/IntelliJ IDE 中添加所需的 VM 参数,如以下示例所示:
-Xms1G -Xmx6G -Dorg.bytedeco.javacpp.maxbytes=16G -Dorg.bytedeco.javacpp.maxphysicalbytes=20G
例如,在 IntelliJ IDE 中,我们可以将 VM 参数添加到运行时配置中:
- 在更改内存限制以适应硬件后,运行以下命令(用于命令行执行):
java -Xms1G -Xmx6G -Dorg.bytedeco.javacpp.maxbytes=16G -Dorg.bytedeco.javacpp.maxphysicalbytes=20G YourClassName
- 配置 JVM 的服务器风格代际垃圾回收器:
java -XX:+UseG1GC
- 使用 ND4J 减少垃圾回收器调用的频率:
Nd4j.getMemoryManager().setAutoGcWindow(3000);
- 禁用垃圾回收器调用,而不是执行第 4 步:
Nd4j.getMemoryManager().togglePeriodicGc(false);
- 在内存映射文件中分配内存块,而不是使用 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:此选项指定可以分配给应用程序的最大字节数。通常,这个值比-Xmx和maxbytes的组合值要大。
假设我们想要在堆内最初配置 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 系统,
maxbytes和maxphysicalbytes是重要的内存限制设置。这里我们处理的是堆外内存。为这些设置分配合理的内存允许我们使用更多的 GPU 资源。 -
对于涉及内存分配问题的
RunTimeException,一个可能的原因是堆外内存空间不可用。如果我们没有使用 设置堆空间和垃圾回收 章节中讨论的内存限制(堆外内存空间)设置,堆外内存空间可能会被 JVM 垃圾回收器回收,从而导致内存分配问题。 -
如果你的环境内存有限,建议不要为
-Xmx和-Xms选项设置过大的值。例如,如果我们为 8 GB 内存的系统使用-Xms6G,那么仅剩下 2 GB 的内存空间用于堆外内存、操作系统和其他进程。
另见
- 如果你有兴趣了解更多关于 G1GC 垃圾回收器的调优内容,可以阅读以下链接:
www.oracle.com/technetwork/articles/java/g1gc-1984535.html
使用异步 ETL
我们使用同步 ETL 进行演示。但在生产环境中,推荐使用异步 ETL。在生产环境中,一个低性能的 ETA 组件可能会导致性能瓶颈。在 DL4J 中,我们使用 DataSetIterator 将数据加载到磁盘。它可以从磁盘、内存中加载数据,或者简单地异步加载数据。异步 ETL 在后台使用异步加载器。通过多线程,它将数据加载到 GPU/CPU 中,其他线程负责计算任务。在下面的操作步骤中,我们将在 DL4J 中执行异步 ETL 操作。
如何操作...
- 使用异步预取创建异步迭代器:
DatasetIterator asyncIterator = new AsyncMultiDataSetIterator(iterator);
- 使用同步预取创建异步迭代器:
DataSetIterator shieldIterator = new AsyncShieldDataSetIterator(iterator);
它是如何工作的...
在第一步中,我们使用 AsyncMultiDataSetIterator 创建了一个迭代器。我们可以使用 AsyncMultiDataSetIterator 或 AsyncDataSetIterator 来创建异步迭代器。AsyncMultiDataSetIterator 有多种配置方式。你可以通过传递其他属性来创建 AsyncMultiDataSetIterator,例如 queSize(一次可以预取的迷你批次的数量)和 useWorkSpace(布尔类型,表示是否应该使用工作区配置)。在使用 AsyncDataSetIterator 时,我们会在调用 next() 获取下一个数据集之前使用当前数据集。还需要注意的是,在没有调用 detach() 的情况下不应存储数据集。如果这样做,数据集中 INDArray 数据使用的内存最终会在 AsyncDataSetIterator 中被覆盖。对于自定义迭代器实现,确保你在训练/评估过程中不要通过 next() 调用初始化大型对象。相反,应将所有初始化工作放在构造函数内,以避免不必要的工作区内存消耗。
在步骤 2 中,我们使用AsyncShieldDataSetIterator创建了一个迭代器。要选择退出异步预取,我们可以使用AsyncShieldMultiDataSetIterator或AsyncShieldDataSetIterator。这些包装器将在数据密集型操作(如训练)中防止异步预取,可以用于调试目的。
如果训练实例每次运行时都执行 ETL 操作,实际上我们每次都在重新创建数据。最终,整个过程(训练和评估)会变得更慢。我们可以通过使用预先保存的数据集来更好地处理这一点。我们在上一章中讨论了使用ExistingMiniBatchDataSetIterator进行预保存,当时我们预保存了特征数据,并随后使用ExistingMiniBatchDataSetIterator加载它。我们可以将其转换为异步迭代器(如步骤 1 或步骤 2 所示),一举两得:使用异步加载的预保存数据。这本质上是一个性能基准,进一步优化了 ETL 过程。
还有更多...
假设我们的迷你批次有 100 个样本,并且我们将queSize设置为10;每次将预取 1,000 个样本。工作区的内存需求取决于数据集的大小,这来自于底层的迭代器。工作区将根据不同的内存需求进行调整(例如,长度变化的时间序列)。请注意,异步迭代器是通过LinkedBlockingQueue在内部支持的。这个队列数据结构以先进先出(FIFO)模式对元素进行排序。在并发环境中,链式队列通常比基于数组的队列有更高的吞吐量。
使用 arbiter 监控神经网络行为
超参数优化/调优是寻找学习过程中超参数的最优值的过程。超参数优化部分自动化了使用某些搜索策略来寻找最佳超参数的过程。Arbiter 是 DL4J 深度学习库的一部分,用于超参数优化。Arbiter 可以通过调整神经网络的超参数来找到高性能的模型。Arbiter 有一个用户界面,用于可视化超参数调优过程的结果。
在这个配方中,我们将设置 arbiter 并可视化训练实例,观察神经网络的行为。
如何操作...
- 在
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>
- 使用
ContinuousParameterSpace配置搜索空间:
ParameterSpace<Double> learningRateParam = new ContinuousParameterSpace(0.0001,0.01);
- 使用
IntegerParameterSpace配置搜索空间:
ParameterSpace<Integer> layerSizeParam = new IntegerParameterSpace(5,11);
- 使用
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 中,一旦我们使用ParameterSpace或OptimizationConfiguration定义了搜索空间,我们需要将其添加到MultiLayerSpace或ComputationGraphSpace中。这些是 DL4J 的MultiLayerConfiguration和ComputationGraphConfiguration的仲裁者对应物。
然后,我们使用candidateGenerator()构建方法添加了candidateGenerator。candidateGenerator为超参数调优选择候选者(各种超参数组合)。它可以使用不同的方法,如随机搜索和网格搜索,来选择下一个用于超参数调优的配置。
scoreFunction()指定在超参数调优过程中用于评估的评估指标。
terminationConditions()用于指定所有的训练终止条件。超参数调优随后将进行下一个配置。
执行超参数调优
一旦使用ParameterSpace或OptimizationConfiguration定义了搜索空间,并且有可能的值范围,下一步是使用MultiLayerSpace或ComputationGraphSpace完成网络配置。之后,我们开始训练过程。在超参数调优过程中,我们会执行多个训练会话。
在这个示例中,我们将执行并可视化超参数调优过程。我们将在演示中使用MultiLayerSpace。
如何实现...
- 使用
IntegerParameterSpace为层大小添加搜索空间:
ParameterSpace<Integer> layerSizeParam = new IntegerParameterSpace(startLimit,endLimit);
- 使用
ContinuousParameterSpace为学习率添加搜索空间:
ParameterSpace<Double> learningRateParam = new ContinuousParameterSpace(0.0001,0.01);
- 使用
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();
- 从
MultiLayerSpace创建candidateGenerator:
Map<String,Object> dataParams = new HashMap<>();
dataParams.put("batchSize",new Integer(10));
CandidateGenerator candidateGenerator = new RandomSearchGenerator(hyperParamaterSpace,dataParams);
- 通过实现
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();
}
}
- 创建一个终止条件数组:
TerminationCondition[] conditions = {
new MaxTimeCondition(maxTimeOutInMinutes, TimeUnit.MINUTES),
new MaxCandidatesCondition(maxCandidateCount)
};
- 计算使用不同配置组合创建的所有模型的得分:
ScoreFunction scoreFunction = new EvaluationScoreFunction(Evaluation.Metric.ACCURACY);
- 创建
OptimizationConfiguration并添加终止条件和评分函数:
OptimizationConfiguration optimizationConfiguration = new OptimizationConfiguration.Builder()
.candidateGenerator(candidateGenerator)
.dataSource(ExampleDataSource.class,dataSourceProperties)
.modelSaver(modelSaver)
.scoreFunction(scoreFunction)
.terminationConditions(conditions)
.build();
- 创建
LocalOptimizationRunner以运行超参数调优过程:
IOptimizationRunner runner = new LocalOptimizationRunner(optimizationConfiguration,new MultiLayerNetworkTaskCreator());
- 向
LocalOptimizationRunner添加监听器,以确保事件正确记录(跳到第 11 步添加ArbiterStatusListener):
runner.addListeners(new LoggingStatusListener());
- 通过调用
execute()方法执行超参数调优:
runner.execute();
- 存储模型配置并将
LoggingStatusListener替换为ArbiterStatusListener:
StatsStorage storage = new FileStatsStorage(new File("HyperParamOptimizationStatsModel.dl4j"));
runner.addListeners(new ArbiterStatusListener(storage));
- 将存储附加到
UIServer:
UIServer.getInstance().attach(storage);
- 运行超参数调优会话,并访问以下 URL 查看可视化效果:
http://localhost:9000/arbiter
- 评估超参数调优会话中的最佳得分,并在控制台中显示结果:
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 步中,我们定义了终止条件。终止条件控制训练过程的进展程度。终止条件可以是MaxTimeCondition、MaxCandidatesCondition,或者我们可以定义自己的终止条件。
在第 7 步中,我们创建了一个评分函数,用于说明在超参数优化过程中如何评估每个模型。
在第 8 步中,我们创建了包含这些终止条件的 OptimizationConfiguration。除了终止条件外,我们还向 OptimizationConfiguration 添加了以下配置:
-
模型信息需要存储的位置
-
之前创建的候选生成器
-
之前创建的数据源
-
要考虑的评估指标类型
OptimizationConfiguration 将所有组件结合起来执行超参数优化。请注意,dataSource() 方法需要两个属性:一个是数据源类的类类型,另一个是我们想要传递的数据源属性(在我们的示例中是 minibatchSize)。modelSaver() 构建方法要求你指定正在训练的模型的存储位置。我们可以将模型信息(模型评分及其他配置)存储在资源文件夹中,然后创建一个 ModelSaver 实例,如下所示:
ResultSaver modelSaver = new FileModelSaver("resources/");
为了使用裁判进行可视化,跳过第 10 步,按照第 12 步操作,然后执行可视化任务运行器。
在遵循第 13 和第 14 步的指示之后,你应该能够看到裁判的 UI 可视化,如下所示:
从裁判可视化中找出最佳模型评分非常直观且容易。如果你运行了多个超参数调优的会话,你可以从顶部的下拉列表中选择特定的会话。此时,UI 上显示的其他重要信息也非常易于理解。