使用深度学习、Groovy™ 和 GraalVM 对鸢尾花进行分类

作者: Paul King

发布时间:2022-06-25 10:52AM(最后更新:2022-06-27 11:16AM)


iris flowers 一个经典的数据集捕获了鸢尾花的特征。它记录了三种山鸢尾变色鸢尾弗吉尼亚鸢尾萼片花瓣宽度长度

groovy-data-science repo 中的Iris 项目专门针对此示例。它包括许多 Groovy 脚本和一个 Jupyter/BeakerX 笔记本,突出显示了此示例,比较和对比了各种库和各种分类算法。

涵盖的技术/库

数据操作

Weka Tablesaw Encog JSAT Datavec Tribuo

分类

Weka Smile Encog Tribuo JSAT Deep Learning4J Deep Netts

可视化

XChart Tablesaw Plot.ly JavaFX

涵盖的主要方面/算法

读取 csv、数据帧、可视化、探索、朴素贝叶斯逻辑回归knn 回归softmax 回归决策树支持向量机

涵盖的其他方面/算法

神经网络多层感知器PCA

如果您对这些额外的技术感兴趣,请随意浏览这些其他示例和 Jupyter/BeakerX 笔记本。

Jupyter/BeakerX notebook image of the Iris problem

对于这篇博客,我们只看深度学习的例子。我们将使用 Encog、Eclipse DeepLearning4J 和 Deep Netts(使用标准 Java 和使用 GraalVM 作为原生镜像)来研究解决方案,但首先简要介绍一下。

深度学习

深度学习属于机器学习人工智能的分支。它涉及人工神经网络的多个层(因此是“深度”)。配置此类网络的方法有很多,细节超出了本博客文章的范围,但我们可以给出一些基本细节。我们将有四个输入节点,对应于我们四个特征的测量值。我们将有三个输出节点,对应于每个可能的类别物种)。我们还将有一个或多个中间附加层。

Iris neural net layers

该网络中的每个节点在某种程度上模仿了人脑中的神经元。同样,我们将简化细节。每个节点都有多个输入,这些输入被赋予特定的权重,以及一个激活函数,它将决定我们的节点是否“触发”。训练模型是一个确定最佳权重的过程。

Neural net node

任何节点将输入转换为输出所涉及的数学并不难。我们可以自己编写(如此处所示,使用矩阵和Apache Commons Math进行数字识别示例),但幸运的是我们不必这样做。我们将要使用的库为我们完成了大部分工作。它们通常提供一个流畅的 API,让我们以一种声明式的方式指定网络中的层。

在探索我们的例子之前,我们应该预先警告大家,虽然我们确实对运行例子进行了计时,但没有尝试严格确保不同技术之间的例子是相同的。不同的技术支持略微不同的设置各自网络层的方式。参数经过调整,因此在运行时,验证中通常最多只有一个或两个错误。此外,运行的初始参数可以用随机或预定义的种子设置。当使用随机种子时,每次运行都会有略微不同的错误。如果我们想在技术之间进行更严格的时间比较,我们需要对例子进行一些额外的对齐并使用像 JMH 这样的框架。尽管如此,它应该能大致指导各种技术的速度。

Encog

Encog 是一个纯 Java 机器学习框架,创建于 2008 年。它还有一个针对 .Net 用户的 C# 版本。Encog 是一个简单的框架,支持一些其他地方找不到的高级算法,但不如其他更近期的框架使用广泛。

我们使用 Encog 进行鸢尾花分类的完整源代码在此处这里,但关键部分是

def model = new EncogModel(data).tap {
    selectMethod(data, TYPE_FEEDFORWARD)
    report = new ConsoleStatusReportable()
    data.normalize()
    holdBackValidation(0.3, true, 1001) // test with 30%
    selectTrainingType(data)
}

def bestMethod = model.crossvalidate(5, true) // 5-fold cross-validation

println "Training error: " + pretty(calculateRegressionError(bestMethod, model.trainingDataset))
println "Validation error: " + pretty(calculateRegressionError(bestMethod, model.validationDataset))

当我们运行示例时,我们看到

paulk@pop-os:/extra/projects/iris_encog$ time groovy -cp "build/lib/*" IrisEncog.groovy
1/5 : Fold #1
1/5 : Fold #1/5: Iteration #1, Training Error: 1.43550735, Validation Error: 0.73302237
1/5 : Fold #1/5: Iteration #2, Training Error: 0.78845427, Validation Error: 0.73302237
...
5/5 : Fold #5/5: Iteration #163, Training Error: 0.00086231, Validation Error: 0.00427126
5/5 : Cross-validated score:0.10345818553910753
Training error:  0.0009
Validation error:  0.0991
Prediction errors:
predicted: Iris-virginica, actual: Iris-versicolor, normalized input: -0.0556, -0.4167,  0.3898,  0.2500
Confusion matrix:            Iris-setosa     Iris-versicolor      Iris-virginica
         Iris-setosa                  19                   0                   0
     Iris-versicolor                   0                  15                   1
      Iris-virginica                   0                   0                  10

real	0m3.073s
user	0m9.973s
sys	0m0.367s

我们不会解释所有统计数据,但它基本上说明我们有一个预测错误率很低的相当好的模型。如果你看到这篇博客前面笔记本图像中的绿色和紫色点,你会发现有些点很难一直正确预测。混淆矩阵显示模型在验证数据集上错误地预测了一朵花。

这个库一个非常好的地方是它是一个单一的 jar 依赖!

Eclipse DeepLearning4j

Eclipse DeepLearning4j 是一套用于在 JVM 上运行深度学习的工具。它支持扩展到 Apache Spark,并且在多个级别上与 Python 集成。它还提供了与 GPU 和 C/++ 库的集成,用于原生集成。

我们使用 DeepLearning4J 进行鸢尾花分类的完整源代码在此处这里,主要部分如下所示

MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder()
    .seed(seed)
    .activation(Activation.TANH) // global activation
    .weightInit(WeightInit.XAVIER)
    .updater(new Sgd(0.1))
    .l2(1e-4)
    .list()
    .layer(new DenseLayer.Builder().nIn(numInputs).nOut(3).build())
    .layer(new DenseLayer.Builder().nIn(3).nOut(3).build())
    .layer(new OutputLayer.Builder(LossFunctions.LossFunction.NEGATIVELOGLIKELIHOOD)
        .activation(Activation.SOFTMAX) // override activation with softmax for this layer
        .nIn(3).nOut(numOutputs).build())
    .build()

def model = new MultiLayerNetwork(conf)
model.init()

model.listeners = new ScoreIterationListener(100)

1000.times { model.fit(train) }

def eval = new Evaluation(3)
def output = model.output(test.features)
eval.eval(test.labels, output)
println eval.stats()

当我们运行这个示例时,我们看到

paulk@pop-os:/extra/projects/iris_encog$ time groovy -cp "build/lib/*" IrisDl4j.groovy
[main] INFO org.nd4j.linalg.factory.Nd4jBackend - Loaded [CpuBackend] backend
[main] INFO org.nd4j.nativeblas.NativeOpsHolder - Number of threads used for linear algebra: 4
[main] INFO org.nd4j.nativeblas.Nd4jBlas - Number of threads used for OpenMP BLAS: 4
[main] INFO org.nd4j.linalg.api.ops.executioner.DefaultOpExecutioner - Backend used: [CPU]; OS: [Linux]
...
[main] INFO org.deeplearning4j.optimize.listeners.ScoreIterationListener - Score at iteration 0 is 0.9707752535968273
[main] INFO org.deeplearning4j.optimize.listeners.ScoreIterationListener - Score at iteration 100 is 0.3494968712782093
...
[main] INFO org.deeplearning4j.optimize.listeners.ScoreIterationListener - Score at iteration 900 is 0.03135504326480282

========================Evaluation Metrics========================
 # of classes:    3
 Accuracy:        0.9778
 Precision:       0.9778
 Recall:          0.9744
 F1 Score:        0.9752
Precision, recall & F1: macro-averaged (equally weighted avg. of 3 classes)

=========================Confusion Matrix=========================
  0  1  2
----------
 18  0  0 | 0 = 0
  0 14  0 | 1 = 1
  0  1 12 | 2 = 2

Confusion matrix format: Actual (rowClass) predicted as (columnClass) N times
==================================================================

real	0m5.856s
user	0m25.638s
sys	0m1.752s

同样,统计数据显示模型良好。在我们的测试数据集的混淆矩阵中有一个错误。DeepLearning4J 确实拥有令人印象深刻的技术范围,可以在某些场景中用于提高性能。对于这个示例,我启用了 AVX(高级矢量扩展)支持,但没有尝试使用 CUDA/GPU 支持,也没有利用任何 Apache Spark 集成。GPU 选项可能会加速应用程序,但考虑到数据集的大小和训练我们网络所需的计算量,它可能不会加速太多。对于这个小例子,为了访问原生 C++ 实现等而设置管道的开销,超过了收益。这些功能通常在处理更大的数据集或大量的计算时才能发挥作用;例如密集视频处理就会想到。

令人印象深刻的扩展选项的缺点是增加了复杂性。代码比我们在这篇博客中看到的其他技术稍微复杂一些,这是基于 API 中某些假设的,如果我们要使用 Spark 集成,即使我们这里没有使用,也需要这些假设。好消息是,一旦工作完成,如果我们确实想使用 Spark,那现在就相对简单了。

复杂性增加的另一个原因是类路径中所需的 jar 文件数量。我选择了使用 `nd4j-native-platform` 依赖项的简单选项,并添加了 `org.nd4j:nd4j-native:1.0.0-M2:linux-x86_64-avx2` 依赖项以支持 AVX。这让我的生活变得轻松,但引入了 170 多个 jar,其中许多用于不需要的平台。如果其他平台的用户也想尝试这个示例,拥有所有这些 jar 非常棒,但对于某些在某些平台上使用长命令行会中断的工具来说,可能会有点麻烦。如果这真的成为一个问题,我当然可以做更多的工作来缩小这些依赖列表。

(对于感兴趣的读者,groovy-data-science 仓库还有其他 DeepLearning4J 示例。Weka 库可以包装 DeepLearning4J,如这个 Iris 示例所示此处。还有我们前面提到的数字识别示例的两个变体,分别使用一层两层神经网络。)

Deep Netts

Deep Netts 是一家提供一系列与深度学习相关的产品和服务的公司。这里我们使用的是免费开源的Deep Netts 社区版纯 Java 深度学习库。它支持 Java 视觉识别 API (JSR381)。JSR381 的专家组于今年早些时候发布了最终规范,所以希望我们很快能看到更多兼容的实现。

我们使用 Deep Netts 进行鸢尾花分类的完整源代码在此,重要部分如下

var splits = dataSet.split(0.7d, 0.3d)  // 70/30% split
var train = splits[0]
var test = splits[1]

var neuralNet = FeedForwardNetwork.builder()
    .addInputLayer(numInputs)
    .addFullyConnectedLayer(5, ActivationType.TANH)
    .addOutputLayer(numOutputs, ActivationType.SOFTMAX)
    .lossFunction(LossType.CROSS_ENTROPY)
    .randomSeed(456)
    .build()

neuralNet.trainer.with {
    maxError = 0.04f
    learningRate = 0.01f
    momentum = 0.9f
    optimizer = OptimizerType.MOMENTUM
}

neuralNet.train(train)

new ClassifierEvaluator().with {
    println "CLASSIFIER EVALUATION METRICS\n${evaluate(neuralNet, test)}"
    println "CONFUSION MATRIX\n$confusionMatrix"
}

当我们运行此命令时,我们看到

paulk@pop-os:/extra/projects/iris_encog$ time groovy -cp "build/lib/*" Iris.groovy
16:49:27.089 [main] INFO deepnetts.core.DeepNetts - ------------------------------------------------------------------------
16:49:27.091 [main] INFO deepnetts.core.DeepNetts - TRAINING NEURAL NETWORK
16:49:27.091 [main] INFO deepnetts.core.DeepNetts - ------------------------------------------------------------------------
16:49:27.100 [main] INFO deepnetts.core.DeepNetts - Epoch:1, Time:6ms, TrainError:0.8584314, TrainErrorChange:0.8584314, TrainAccuracy: 0.5252525
16:49:27.103 [main] INFO deepnetts.core.DeepNetts - Epoch:2, Time:3ms, TrainError:0.52278274, TrainErrorChange:-0.33564866, TrainAccuracy: 0.52820516
...
16:49:27.911 [main] INFO deepnetts.core.DeepNetts - Epoch:3031, Time:0ms, TrainError:0.029988592, TrainErrorChange:-0.015680967, TrainAccuracy: 1.0
TRAINING COMPLETED
16:49:27.911 [main] INFO deepnetts.core.DeepNetts - Total Training Time: 820ms
16:49:27.911 [main] INFO deepnetts.core.DeepNetts - ------------------------------------------------------------------------
CLASSIFIER EVALUATION METRICS
Accuracy: 0.95681506 (How often is classifier correct in total)
Precision: 0.974359 (How often is classifier correct when it gives positive prediction)
F1Score: 0.974359 (Harmonic average (balance) of precision and recall)
Recall: 0.974359 (When it is actually positive class, how often does it give positive prediction)

CONFUSION MATRIX
                          none    Iris-setosaIris-versicolor Iris-virginica
           none              0              0              0              0
    Iris-setosa              0             14              0              0
Iris-versicolor              0              0             18              1
 Iris-virginica              0              0              0             12

real	0m3.160s
user	0m10.156s
sys	0m0.483s

这比 DeepLearning4j 快,与 Encog 相似。考虑到我们的小数据集,这是意料之中的,并不能说明大型问题的性能。

另一个优点是依赖列表。它不像 Encog 那样是单个 jar,但相差不远。有 Encog jar,JSR381 VisRec API(在一个单独的 jar 中),以及一些日志 jar。

使用 GraalVM 的 Deep Netts

如果性能对我们很重要,我们可能需要考虑的另一项技术是 GraalVM。GraalVM 是一个高性能 JDK 发行版,旨在加速用 Java 和其他 JVM 语言编写的应用程序的执行。我们将看看如何创建我们的 Iris Deep Netts 应用程序的原生版本。我们使用了 GraalVM 22.1.0 Java 17 CE 和 Groovy 4.0.3。我们将只介绍基本步骤,但还有其他地方可以获取额外的设置信息和故障排除帮助,例如此处此处此处

Groovy 有两种特性。它的动态特性支持通过元编程在运行时添加方法,并通过缺失方法拦截和其他技巧与方法分派处理进行交互。其中一些技巧大量使用了反射和动态类加载,并给 GraalVM 带来了问题,因为 GraalVM 试图在编译时确定尽可能多的信息。Groovy 的静态特性具有更有限的元编程能力,但允许生成更接近 Java 的字节码。幸运的是,我们的示例不依赖任何动态 Groovy 技巧。我们将使用静态模式进行编译

paulk@pop-os:/extra/projects/iris_encog$ groovyc -cp "build/lib/*" --compile-static Iris.groovy

接下来我们构建我们的原生应用程序

paulk@pop-os:/extra/projects/iris_encog$ native-image  --report-unsupported-elements-at-runtime \
   --initialize-at-run-time=groovy.grape.GrapeIvy,deepnetts.net.weights.RandomWeights \
   --initialize-at-build-time --no-fallback  -H:ConfigurationFileDirectories=conf/  -cp ".:build/lib/*" Iris

我们告诉 GraalVM 在运行时初始化 `GrapeIvy`(以避免在类路径中需要 Ivy jar,因为 Groovy 只会在我们使用 `@Grab` 语句时惰性加载这些类)。我们对 `RandomWeights` 类也做了同样的处理,以避免它在编译时被锁定在一个固定的随机种子中。

现在我们准备运行我们的应用程序了

paulk@pop-os:/extra/projects/iris_encog$ time ./iris
...
CLASSIFIER EVALUATION METRICS
Accuracy: 0.93460923 (How often is classifier correct in total)
Precision: 0.96491224 (How often is classifier correct when it gives positive prediction)
F1Score: 0.96491224 (Harmonic average (balance) of precision and recall)
Recall: 0.96491224 (When it is actually positive class, how often does it give positive prediction)

CONFUSION MATRIX
                          none    Iris-setosaIris-versicolor Iris-virginica
           none              0              0              0              0
    Iris-setosa              0             21              0              0
Iris-versicolor              0              0             20              2
 Iris-virginica              0              0              0             17

real    0m0.131s
user    0m0.096s
sys     0m0.029s

我们可以在这里看到速度显著提高。这很棒,但我们应该注意,使用 GraalVM 通常涉及一些棘手的调查,特别是对于默认具有动态特性的 Groovy。使用 Groovy 的静态特性时,Groovy 的一些功能将不可用,并且一些库可能会出现问题。例如,Deep Netts 的依赖项之一是 log4j2。在撰写本文时,使用 log4j2 和 GraalVM 仍然存在问题。我们排除了 `log4j-core` 依赖项,并使用 `log4j-to-slf4j` 和 `logback-classic` 来规避这个问题。

结论

我们已经看到了一些不同的库,用于使用 Groovy 执行深度学习分类。每个库都有其优点和缺点。当然,有多种选项可以满足人们对极速启动速度的需求,直到可扩展到云中大规模计算农场的选项。

更新历史

2022 年 9 月 27 日:我将 Deep Netts GraalVM 的 iris 应用程序以及更详细的说明放入了它自己的子项目中。