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

作者:_Paul King_
发布时间:2022-06-25 上午 10:52(上次更新时间:2022-06-27 上午 11:16)


鸢尾花 经典数据科学数据集记录了鸢尾花的特征。它记录了三种_物种_(山鸢尾变色鸢尾维吉尼亚鸢尾)的_萼片_和_花瓣_的_宽度_和_长度_。

Iris 项目位于groovy-data-science 存储库中,专门用于此示例。它包含许多 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 repo 中还有其他 DeepLearning4J 示例。如这个 Iris 示例这里所示,Weka 库可以封装 DeepLearning4J。我们之前提到的数字识别示例也有两种变体,分别使用一层两层神经网络。)

Deep Netts

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

我们使用 Deep Netts 进行 Iris 分类示例的完整源代码在这里,重要部分如下

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 文件、位于单独 jar 文件中的 JSR381 VisRec API 以及一些日志记录 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 的一些特性在使用其静态特性时将不可用,并且某些库可能会出现问题。例如,Deep Netts 将 log4j2 作为其依赖项之一。在撰写本文时,将 log4j2 与 GraalVM 一起使用仍然存在问题。我们排除了 log4j-core 依赖项,并使用由 logback-classic 支持的 log4j-to-slf4j 来避开此问题。

结论

我们已经看到了一些使用 Groovy 执行深度学习分类的不同库。每个都有自己的优点和缺点。当然可以选择满足那些希望以极快的启动速度运行的用户,以及可以选择扩展到云中大型计算集群的用户。

更新历史

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