整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

DJL 之 Java 玩转多维数组,就像 NumPy

DJL 之 Java 玩转多维数组,就像 NumPy 一样

文适合有 Java 基础的人群

作者:DJL-Lanking

HelloGitHub 推出的《讲解开源项目》系列。有幸邀请到了亚马逊 + Apache 的工程师:Lanking( https://github.com/lanking520 ),为我们讲解 DJL —— 完全由 Java 构建的深度学习平台,本文为本系列的第二篇。

一、前言

随着数据科学在生产中的应用逐步增加,使用 N维数组 灵活的表达数据变得愈发重要。我们可以将过去数据科学运算中的多维循环嵌套运算简化为简单几行。由于进一步释放了计算并行能力,这几行简单的代码运算速度也会比传统多维循环快很多。

这种数学计算的包已经成为数据科学、图形学以及机器学习领域的标准。同时它的影响力还在不断的扩大到其他领域。

在 Python 的世界,调用 NDArray(N维数组)的标准包叫做 NumPy。但是如今在 Java 领域中,并没有与之同样标准的库。为了给 Java 开发者创造同一种使用环境,亚马逊云服务开源了 DJL 一个基于 Java 的深度学习库。

尽管它包含了深度学习模块,但是它最核心的 NDArray 系统可以被用作 N维数组 的标准。它具备优良的可扩展性、全平台支持以及强大的后端引擎支持 (TensorFlow、PyTorch、Apache MXNet)。无论是 CPU 还是 GPU、PC 还是安卓,DJL 都可以轻而易举的完成任务。

项目地址:https://github.com/awslabs/djl/

在这个文章中,我们将带你了解 NDArray,并且教你如何写与 Numpy 同样简单的 Java 代码以及如何将 NDArray 使用在现实中的应用之中。

二、安装 DJL

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

在线 JShell 链接: https://djl.ai/website/demo.html#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

3.1 创建 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 创建的方法。

NDManager 文档:https://javadoc.io/doc/ai.djl/api/latest/ai/djl/ndarray/NDManager.html

3.2 数学运算

你可以使用 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 数学运算,基本涵盖了大部分的应用场景。

3.3 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]
*/

是不是非常简单?接下来,我们看一下一个稍微复杂一些的应用场景。假设我们现在有一个3x3的矩阵,然后我们想把第二列的数据都乘以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 被应用在了对于数据进行前后处理的工作中。

4.1 分词操作

在输入到 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 }
*/

4.2 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 究竟是如何在 DJL 之中构建的呢?接下来,我们会讲解一下 NDArray 在 DJL 内部中的架构。架构图如下:

如上图所示 NDArray 有三个关键的层。

界面层 (Interface) 包含了你所用到的 NDArray ,它只是一个 Java 的界面并定义了 NDArray 的输入输出结构。我们很仔细的分析了每一个方式的使用方法以便尽可能的将它们和用户的应用场景统一以及便于使用。

在引擎提供者层 (EngineProvider),是 DJL 各种深度学习引擎为 NDArray 界面开发的包。这个层把原生的深度学习引擎算子表达映射在 NumPy 之上。这样经过这样一层转译,我们在不同引擎上看到 NDArray 的表现都是一致的而且同时兼顾了 NumPy 的表现。

在 C++ 层,为了更便于 Java 使用,我们构建了 JNI 和 JNA 暴露出 C/C++ 的等方法,它可以保证我们有足够的方法来构建 NDArray 所需要的功能。同时 C++ 与 Java 的直接调用也可以保证 NDArray 拥有最好的性能。

六、为什么应该使用 NDArray 呢?

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

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

NDArray 的到来帮助 DJL 成功转变为 Java 在深度学习领域中最好的工具。它具备平台自检测机制,无需任何额外设置,便可以在应用中构建基于 CPU/GPU 的代码。感兴趣的小伙伴快跟着教程感受下吧!

更多详情尽在 NDArray 文档:https://javadoc.io/doc/ai.djl/api/latest/ai/djl/ndarray/NDArray.html


关于 DJL

Deep Java Library (DJL) 是一个基于 Java 的深度学习框架,同时支持训练以及推理。DJL 博取众长,构建在多个深度学习框架之上 (TenserFlow、PyTorch、MXNet 等) 也同时具备多个框架的优良特性。你可以轻松使用 DJL 来进行训练然后部署你的模型。

它同时拥有着强大的模型库支持:只需一行便可以轻松读取各种预训练的模型。现在 DJL 的模型库同时支持高达 70 个来自 GluonCV、 HuggingFace、TorchHub 以及 Keras 的模型。

项目地址:https://github.com/awslabs/djl/

在最新的版本中 DJL 0.6.0 添加了对于 MXNet 1.7.0、PyTorch 1.5.0、TensorFlow 2.2.0 的支持。我们同时也添加了 ONNXRuntime 以及 PyTorch 在安卓平台的支持。


源:升学就业帮讲师——肖云锐

1. 数组的声明和赋值

方式一: new Array()构造函数方法

// 1. 使用构造函数创建数组对象
// 创建了一个空数组var arr=new Array();
// 创建了一个数组,里面存放了3个字符串
var arr=new Array('zs', 'ls', 'ww');
// 创建了一个数组,里面存放了4个数字
var arr=new Array(1, 2, 3, 4);

方式二: 字面量方式

// 2. 使用字面量创建数组对象
var arr=[1, 2, 3];
// 获取数组中元素的个数
console.log(arr.length);

注意事项:

1. 定义空数组的方式

var arr1=[];

2. 定义一个数组可以存入不同的数据类型. 但是一般不建议这样使用.

var arr3=[25,true,'abc'];

3. 访问数组的元素通过索引,索引从开始

var arr6 = [];
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
console.log(arr6);

4. js中数组的下标是可以不连续的,如果不连续默认补充empty

var arr6 = [];
arr6[0] = 10;
arr6[1] = 20;
arr6[2] = 30;
console.log(arr6);
arr6[4] = 50;
console.log(arr6)

执行结果如下图所示:

5. 数组的扩容和缩容

var arr = [1, 1.2, new Date(), false, "呵呵"];
console.log("前:" + arr);
// 数组的扩容arr.length = 10;
// 数组的缩小
//arr.length = 3;
console.log("后:" + arr);

6. 清空数组

// 方式1 推荐
arr=[];
// 方式2
arr.length=0;
// 方式3
arr.splice(0, arr.length);

2. 数组的遍历

方式一:for循环,也是最常见的

for (let i=0; i < arr.length; i++) {
console.log(arr[i])
}

方式二:for......in 遍历数组

for(let item in arr){
console.log(arr[item])
}

方式三: foreach遍历数组


arr.forEach(function(item, index){ 
   console.log(item + "=" + index);
});

3. 数组的常用方法

数组常用的一些方法:

首尾数据操作

push() //在数组末尾添加一个或多个元素,并返回数组操作后的长度
pop() //删除数组最后一项,返回删除项
shift() //删除数组第一项,返回删除项
unshift() //在数组开头添加一个或多个元素,并返回数组的新长度

合并和拆分

concat()
// 将两个数组合并成一个新的数组,原数组不受影响。
// 参数位置可以是一个数组字面量、数组变量、零散的值。
slice(start,end)
// 从当前数组中截取一个新的数组,不影响原来的数组,返回一个新的数组,
// 包含从 start 到 end (不包括该元素)的元素。
// 参数区分正负,正值表示下标位置,负值表示从后面往前数第几个位置,
// 参数可以只传递一个,表示从开始位置截取到字符串结尾。

删除、插入、替换

splice(index,howmany,element1,element2,……)
//用于插入、删除或替换数组的元素
//index:删除元素的开始位置
//howmany:删除元素的个数,可以是0
//element1,element2:要替换的新的数据。

位置方法

indexOf() //查找数据在数组中最先出现的下标
lastIndexOf() //查找数据在数组中最后一次出现的下标
//如果没找到返回-1

排序和倒序

reverse() //将数组完全颠倒,第一项变成最后一项,最后一项变成第一项。
sort(); //默认根据字符编码顺序,从小到大排序
//如果想要根据数值大小进行排序,必须添加sort的比较函数参数。
//该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。比较函数应该具有两个参数 a 和 b,根据a和b的关系作为判断条件,返回值根据条件分为三个分支,正数、负数、0:
//返回值是负数-1:a排在b前面。
//返回值是整数1:a排在b后面。
//返回值是0:a和b的顺序保持不变。
//人为能控制的是判断条件。

转字符串方法

// 将数组的所有元素连接到一个字符串中。
join() //通过参数作为连字符将数组中的每一项用连字符连成一个完整的字符串

迭代方法

//不会修改原数组(可选) HTML5新增
every()、filter()、forEach()、map()、some()

代码示例

近给京东2022秋招做了一道算法题。 问题需要输出的数据格式是二维数组。 但是我的回答在ac时有问题,二维数组的每个子数组中的值都是一样的。 当时一直卡在寻找二层for循环的bug,但是忽略了问题出在我定义二维数组的方式上,所以这里想讲一下如何定义一个真正的二- 维数组。

我们先看一个例子,网上最常用的定义二维数组的方法

const n=3
 let arr=new  Array(n). fill( new  Array(n). fill( 0));
 console. log(arr)
 copy code

看起来是对的,数组里面嵌套了三个数组,和我们想象的二维数组一样,但是问题就在这里。

看起来对吗? 我们来做一个操作给二维数组中的元素赋值

arr[ 0][ 1]=1;
 console. log( "after assignment", arr);
 copy code

我勒个去!什么情况下我只给二维数组的第0行第1列赋值1。为什么每个的第一列都变成1?

相信很多人第一次遇到这种情况时,都会有和我一样的疑惑。问题出在哪里?


问题在于 Array.fill()

不熟悉的朋友可以看看MDN上的简单解释

Array.prototype.fill()

我希望你会注意到这句话:“当一个对象被传递给 fill 方法时,数组被填充了对该对象的引用。 "

注意:如果填充的内容是'object'类型,那么对象分配了相同的内存地址,而不是深拷贝对象。

这就解释了上面的问题。我们根据它的内存地址找到了对象并修改了对象本身。这样,其他子数组也发生了变化,因为这些子数组都取自同一个内存地址。因此,上述方法定义的二维数组不能称为真正的二维数组(我这里说的对象其实就是js中的引用类型数据,array、object、map、Set,这些都是。 )


如何定义一个真正的二维数组?

其实很简单,两个for循环就可以完成

let arr1=new  Array(n);
 for ( let i=0; i < n; i++) {
   for ( let j=0; j < n; j++) {
    arr1[i]=new  Array(n). fill( 0);
  }
}
 copy code

让我们给它赋值,看看之前和之后的结果

console. log( "before assignment", arr1);
arr1[ 0][ 1]=1;
 console. log( "after assignment", arr1);
 copy code

没错,我们定义了一个真正的二维数组

希望大家以后在遇到二维数组问题的时候放弃第一种写法。 我们需要的是一个真正的二维数组。


关注七爪网,获取更多APP/小程序/网站源码资源!