DJL 之 Java 玩轉多維陣列,就像 NumPy 一樣

削微寒發表於2020-09-03

本文適合有 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 建立的方法。

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 在安卓平臺的支援。


關注 HelloGitHub 公眾號

相關文章