亚马逊AWS官方博客

NDArray — 基于 Java 的 N 维数组工具

前言

随着数据科学在生产中的应用逐步增加,使用N维数组灵活的表达数据变得愈发重要。我们可以将过去数据科学运算中的多维循环嵌套运算简化为简单几行。由于进一步释放了计算并行能力,这几行简单的代码运算速度也会比传统多维循环快很多。这种数学计算的包已经成为于数据科学,图形学以及机器学习领域的标准。同时它的影响力还在不断的扩大到其他领域。 在Python的世界,调用NDArray的标准包叫做NumPy。但是如今在Java领域中,并没有与之同样标准的库。为了给Java开发者创造同一种使用环境,亚马逊云服务开源了DJL,一个基于Java的深度学习库。尽管它包含了深度学习模块,但是它最核心的NDArray系统可以被用作N维数组的标准。它具备优良的可扩展性,全平台支持,以及强大的后端引擎支持 (TensorFlow, PyTorch, Apache MXNet)。无论是CPU还是GPU, PC还是安卓,DJL都可以轻而易举的完成任务。 在这个文章中,我们将带你了解NDArray,并且教你如何写与Numpy同样简单的Java代码以及如何将NDArray使用在现实中的应用之中。

安装DJL

可以通过下方的配置来配置你的gradle项目。或者,你也可以跳过设置直接使用我们在线JShell

plugins {
id 'java'
}
repositories {
jcenter()
}
dependencies {
implementation "ai.djl:api:0.6.0"
// PyTorch
runtimeOnly "ai.djl.pytorch:pytorch-engine:0.6.0"
runtimeOnly "ai.djl.pytorch:pytorch-native-auto:1.5.0"
}

然后,我们就可以开始上手写代码了。

基本操作

我们首先尝试建立一个try block来包含我们的代码(如果使用在线JShell可跳过此步):

try(NDManager manager = NDManager.newBaseManager()) {
}

NDManager是DJL中的一个class可以帮助管理NDArray的内存使用。通过创建NDManager,我们可以更及时的对内存进行清理。当这个block里的任务运行完成时,内部产生的NDArray都会被清理掉。这个设计保证了我们在大规模使用NDArray的过程中,可以通过清理其中的NDManager来更高效的利用内存。 为了做对比,我们可以参考NumPy在Python之中的应用。

import numpy as np

创建NDArray

ones 是一个创建全是1的N维数组操作. Python (Numpy)

nd = np.ones((2, 3))
```
[[1. 1. 1.]
[1. 1. 1.]]
```

Java (DJL NDArray)

NDArray nd = manager.ones(new Shape(2, 3));
/*
ND: (2, 3) cpu() float32
[[1., 1., 1.],
[1., 1., 1.],
]
*/

你也可以尝试生成随机数。比如我们需要生成一些从0到1的随机数: Python (Numpy)

nd = np.random.uniform(0, 1, (1, 1, 4))
# [[[0.7034806 0.85115891 0.63903668 0.39386125]]]

Java (DJL NDArray)

NDArray nd = manager.randomUniform(0, 1, new Shape(1, 1, 4));
/*
ND: (1, 1, 4) cpu() float32
[[[0.932 , 0.7686, 0.2031, 0.7468],
],
]
*/

这只是简单演示一些常用功能。现在NDManager支持多达20种在NumPy中NDArray创建的方法

数学运算

你可以使用NDArray进行一系列的数学操作。假设你想做对数据做一个转置操作,然后对所有数据加一个数的操作。你可以参考如下的实现:

Python (Numpy)

nd = np.arange(1, 10).reshape(3, 3)
nd = nd.transpose()
nd = nd + 10
```
[[11 14 17]
[12 15 18]
[13 16 19]]
```

Java (DJL NDArray)

NDArray nd = manager.arange(1, 10).reshape(3, 3);
nd = nd.transpose();
nd = nd.add(10);
/*
ND: (3, 3) cpu() int32
[[11, 14, 17],
[12, 15, 18],
[13, 16, 19],
]
*/

DJL现在支持60多种不同的NumPy数学运算,基本涵盖了大部分的应用场景。

Get 和 Set

其中一个对于NDArray最重要的亮点就是它轻松简单的数据设置/获取功能。我们参考了NumPy的设计,将Java过去对于数据表达中的困难做了精简化处理。 假设我们想筛选一个N维数组所有小于10的数:

Python (Numpy)

nd = np.arange(5, 14)
nd = nd[nd >= 10]
# [10 11 12 13]

Java (DJL NDArray)

NDArray nd = manager.arange(5, 14);
nd = nd.get(nd.gte(10));
/*
ND: (4) cpu() int32
[10, 11, 12, 13]
*/

是不是非常简单?接下来,我们看一下一个稍微复杂一些的应用场景。假设我们现在有一个3×3的矩阵,然后我们想把第二列的数据都乘以2:

Python (Numpy)

nd = np.arange(1, 10).reshape(3, 3)
nd[:, 1] *= 2
```
[[ 1 4 3]
[ 4 10 6]
[ 7 16 9]]
```

Java (DJL NDArray)

NDArray nd = manager.arange(1, 10).reshape(3, 3);
nd.set(new NDIndex(":, 1"), array -> array.mul(2));
/*
ND: (3, 3) cpu() int32
[[ 1, 4, 3],
[ 4, 10, 6],
[ 7, 16, 9],
]
*/

在上面的案例中,我们在Java引入了一个NDIndex的class。它复刻了大部分在NumPy中对于NDArray支持的get/set操作。只需要简单的放进去一个字符串表达式,开发者在Java中可以轻松玩转各种数组的操作。

现实中的应用场景

上述的操作对于庞大的数据集是十分有帮助的。现在我们来看一下这个应用场景:基于单词的分类系统训练。在这个场景中,开发者想要利用从用户中获取的数据来进行情感分析预测。NDArray被应用在了对于数据进行前后处理的工作中。

分词操作

在输入到NDArray数据前,我们需要对于输入的字符串进行分词操作并编码成数字。下面代码中看到的tokenizer 是一个 Map<String, Integer>。它是一个单词到字典位置的映射。

String text = "The rabbit cross the street and kick the fox";
String[] tokens = text.toLowerCase().split(" ");
int[] vector = new int[tokens.length];
/*
String[9] { "the", "rabbit", "cross", "the", "street",
"and", "kick", "the", "fox" }
*/
for (int i = 0; i < tokens.length; i++) {
vector[i] = tokenizer.get(tokens[i]);
}
vector
/*
int[9] { 1, 6, 5, 1, 3, 2, 8, 1, 12 }
*/

NDArray处理

经过了编码操作后,我们创建了 NDArray. 然后,我们需要转化数据的结构:

NDArray array = manager.create(vector);
array = array.reshape(new Shape(vector.length, 1)); // form a batch
array = array.div(10.0);
/*
ND: (9, 1) cpu() float64
[[0.1],
[0.6],
[0.5],
[0.1],
[0.3],
[0.2],
[0.8],
[0.1],
[1.2],
]
*/

最后,我们将数据传入深度学习模型中。如果使用Java要达到这些需要更多的工作量: 如果我们需要实现类似于reshape的方法,我们需要创建一个N维数组:List<List<List<...List<Float>...>>> 来保证不同维度的可操作性。同时我们需要能够支持插入新的 List<Float> 来创建最终的数据格式。

为什么应该使用NDArray呢?

经过了这个教程,你应该获得了基本的NDArray在Java中的使用体验。但是这仍然只是表象,它的很多内在价值只有在生产环境中才能体现出来。总结一下,NDArray具有如下几个优点:

  • 易如反掌: 轻松使用超过 60+ 个在Java中的方式实现与NumPy相同的结果。
  • 快如闪电: 具备各路深度学习框架加持,DJL NDArray具备了各种硬件平台的加速,比如在CPU上的 MKLDNN 加速以及GPU上的CUDA 加速。无论多大的数据集都可以轻松应对。
  • 深度学习:同时具备高维数组支持,同时具备离散数组支持。你可以轻松的将DJL与其他大数据或者流数据平台结合起来应用:比如分布式处理的Apache Spark平台,以及Apache Flink的流数据平台。为你现有的方案构建一层深度学习的中间件。

*离散数组现在只包含 PyTorch中的 COO 以及MXNet中的 CSR/Row_Sparse.

NDArray在DJL中的实现过程

你也许会好奇,NDArray究竟是如何在DJL之中构建的呢?接下来,我们会讲解一下NDArray在DJL内部中的架构。

NDArray架构


如上图所示,NDArray有三个关键的层。 界面层 (Interface) 包含了你所用到的NDArray,它只是一个Java的界面并定义了NDArray的输入输出结构。我们很仔细的分析了每一个方式的使用方法以便尽可能的将它们和用户的应用场景统一以及便于使用。 在引擎提供者层(EngineProvider),是DJL各种深度学习引擎为NDArray界面开发的包。这个层把原生的深度学习引擎算子表达映射在NumPy之上。这样经过这样一层转译,我们在不同引擎上看到NDArray的表现都是一致的而且同时兼顾了NumPy的表现。 在C++层,为了更便于Java使用,我们构建了JNI和JNA暴露出C/C++的等方法,它可以保证我们有足够的方法来构建NDArray所需要的功能。同时C++与Java的直接调用也可以保证NDArray拥有最好的性能。

关于DJL


Deep Java Library (DJL) 是一个基于Java的深度学习框架,同时支持训练以及推理。DJL博取众长,构建在多个深度学习框架之上 (TenserFlow, PyTorch, MXNet, etc)也同时具备多个框架的优良特性。你可以轻松使用DJL来进行训练然后部署你的模型。它同时拥有着强大的模型库支持:只需一行便可以轻松读取各种预训练的模型。现在DJL的模型库同时支持高达70个来自GluonCV, HuggingFace, TorchHub以及Keras的模型。 NDArray的到来帮助DJL成功转变为Java在深度学习领域中最好的工具。它具备平台自检测机制,无需任何额外设置,便可以在应用中构建基于CPU/GPU的代码。 在最新的版本中,DJL 0.6.0添加了对于MXNet 1.7.0, PyTorch 1.5.0 and TensorFlow 2.2.0的支持。我们同时也添加了ONNXRuntime以及PyTorch在安卓平台的支持。 请参考我们的 GitHub, demo repository, Slack channel 以及知乎来获取更多信息!

 

本篇作者

兰青

Qing Lan, AWS AI 软件开发工程师。DJL深度学习框架作者之一,Apache软件基金会项目管理委员会成员。