当前位置: 首页 > article >正文

JavaFX17 现代 Java 客户端权威指南(七)

原文:The Definitive Guide to Modern Java Clients with JavaFX 17

协议:CC BY-NC-SA 4.0

十三、机器学习和 JavaFX

威廉·安东尼奥·西西里

机器学习最近再次成为热门话题,主要是因为大量数据被生成和存储,以及处理能力的提高。机器学习算法远不止是一个研究课题;它们被公司用作竞争优势。在本章中,我们将讨论最著名的机器学习算法,重点是人工神经网络,并展示 JavaFX 如何与可靠的机器学习库 DeepLearning4J (DL4J)一起使用。我们将重点关注可以直接与 JavaFX 交互的视觉神经网络模型。

什么是机器学习

当你开发一个系统时,你必须准确地编写它应该做什么。你开发一个算法,一步一步地描述一个特定的流程必须如何执行。

机器学习技术是不同的,因为它们不需要明确的编程步骤。这些技术无需显式编程即可返回结果。你不是给它编程,而是“教”机器如何使用数据。

在机器学习领域,我们有两种不同类型的算法用于不同的任务,具有不同的性能和精度。我们将这些算法分为两大类:

  • 监督学习

  • 无监督学习

这两个类别都需要数据作为输入。

监督学习

在监督学习中,我们有利用标记数据的算法,这意味着您将为算法提供问题的样本实例,以便它可以学习如何对同一问题的新的未标记实例进行分类。例如,假设你有一些狗和猫的图像,你把这些图像用在一些算法上。在教完算法后,你可以向它输入新的图像,它应该会告诉你新图像中包含的是猫还是狗。

要教算法,需要输入信息,很多信息,调整算法参数,直到它能合理预测新数据。这个过程叫做训练。假设您想在照片中识别您的家人,并且您有数千张您家庭成员的照片。一旦这些照片被正确标记,你就可以用它们来喂给一个算法,一旦算法有了很好的精度,它就可以用来预测新的图片,有希望识别你的家庭成员!

无监督学习

当你有数据,你没有进一步的信息,但你仍然想检索一些知识,你可以使用无监督学习;根据所选的算法,它可以对数据的某些实例进行分组。无监督学习的一个已知示例是推荐系统,在该系统中,您使用特定系统上的用户数据向他们推荐其他产品或电影。

要使用机器学习技术,可以在多种可用算法中进行选择。对于监督学习,我们有回归、决策树等等。对于无监督学习,你会发现聚类,异常检测,等等。对于监督和非监督学习,我们有神经网络,我们将在本章中探讨。

人工神经网络

由于可用于训练的大量数据以及高性能 CPU 和 GPU,人工神经网络非常有名,并得到了高度的讨论和研究。神经网络基本元素是人工神经元,它以“神经神经元”为基础,由输入数(x)乘以其权重(w)并以一个偏差求和而成,结果插入一个激活函数中。然后我们有了结果(y)。这些神经元被组织成可以有 n 个神经元的层。各层以不同的架构连接,最后我们有了如图 13-1 所示的人工神经网络。现在的人工神经网络是由成千上万个神经元组成的,有时有几百层。这些大型人工神经网络是深度学习方法的一部分,我们将在本章中看到一些著名的深度神经网络架构。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-1

神经网络的一个例子

训练过程是神经网络发挥作用的关键。在训练之前,神经网络从随机权重开始。训练包括在神经网络中输入数据,测量它与实际信息的距离,然后调整权重,直到结果接近实际值(也称为地面真实值)。例如,如果你有一个神经网络可以预测给定的图像是猫还是狗,你输入一只猫,它返回的结果是猫有 80%是狗,你计算一个误差(结果离地面真实数据有多远),然后使用称为反向传播的过程来调整神经网络的权重,并用数千只猫和狗的图像重复它,直到你有一个好的结果。在训练期间,你应该关注过度配合,但这超出了本书的范围。

有相当多非常知名的神经网络架构可供使用,其中大部分是由大公司或人工智能研究人员提出的。你可能会创建自己的神经网络,获得大量数据,并对其进行训练,这样你就可以在你的应用中使用它;然而,在本章中,我们将使用预训练的神经网络。是的,优秀的灵魂得到了一些非常著名的神经网络架构,使用一些已知的数据集(例如,ImageNet)训练它们,并且,一旦它们被训练,使它们可用于应用中;这些被称为预训练神经网络模型。

经过预训练的神经网络的强大之处在于,它已经为某个数据集调整了所有权重,这意味着它已经可供使用,并且您可以使用自己的数据再次调整权重,使其准备好处理新的类,从而重用来自其他图像或数据的知识。

卷积神经网络

深入所有神经网络架构和技术超出了本书的范围;然而,由于我们将主要使用卷积神经网络(CNN),其架构对于检测图像中的模式很有用,而不必编写特定的模式,因此我们来讨论一下。为了理解 CNN 是如何工作的,以图 13-2 为例,这是我妻子在一个应用程序中画的一只蜜蜂,我们将在后面讨论。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-2

CNN 要分析的图像示例

观察这只蜜蜂,我们可以识别出一些图案:一只翅膀是曲线,身体也有几条曲线和一个填充的部分,头部是椭圆形,等等。你可能不知道是怎么做到的,但是你的大脑识别出了这些模式,并得出结论,这是一幅蜜蜂的图画。CNN 包含一个卷积层,与池化和规范化层一起使用,可以识别这些模式,您不必对其进行硬编码。这都是在培训过程中学到的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

然后,CNN 层的输出被传递到一个完全连接的架构,该架构将以代表每一类图像(蜜蜂、青蛙、狗等)的神经元结束。).当你实际预测一幅图像时,每一类都有百分之一的机会属于某一类。请看下面的例子,应用程序背后的 CNN 知道我们试图输入一个鼠标图像(大约 78%的鼠标),但它也说有很小的机会是一只狮子(大约 13%),如图 13-3 !

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-3

CNN 进行的图像预测的结果

要了解更多关于 CNN 的信息,请查看斯坦福( http://cs231n.github.io/convolutional-networks/ )的文章和 ImageNet 竞赛获胜者的论文。

Eclipse DeepLearning4J:用于神经网络的 Java API

如果你搜索深度学习和 Java,你会找到一些库。出于我们的目的,我们将使用 Eclipse DeepLearning4J (DL4J),它允许轻松的数据矢量化、神经网络创建和训练,并提供可立即使用的预训练模型,甚至可以在移动设备上运行。

DL4J 使用的核心库是 ND4J。我们可以用 ND4J 进行 n 大小的向量运算;因此,它用于 DL4J 中的所有神经网络运算。例如,您希望为训练或预测而加载的每个数据都被转换为 ND4J INDArray 对象,因此它可以在训练过程中为神经网络提供信息。有关 ND4J 的更多信息,请参见第十四章“使用 JavaFX 的科学应用”

在 ND4J 之上,我们有 DataVec 库。处理神经网络就是处理数据,神经网络上的数据由 n 个大小的数字向量表示。你不能简单地将图像或文本字符串的二进制文件输入到神经网络中。你必须转换它;DataVec 拥有所有的工具,可以将图像、文本、CSV 文件等转换成神经网络。稍后,我们将使用 DataSetIterators,它将清楚地说明它是如何有用的。

使用 Maven 的 DeepLearning4J 设置很简单;你必须添加 nd4j-native-platform 和 deeplearning4j-core 依赖项。对于这一章,我们还需要 deeplearning4j-zoo 来利用可用的神经网络模型。对于本章,我们使用 deeplearning4j 1.0.0-beta7:

    <dependency><groupId>org.nd4j</groupId><artifactId>nd4j-native-platform</artifactId><version>${dl4j.version}</version></dependency><dependency><groupId>org.deeplearning4j</groupId><artifactId>deeplearning4j-core</artifactId><version>${dl4j.version}</version></dependency><dependency><groupId>org.deeplearning4j</groupId><artifactId>deeplearning4j-zoo</artifactId><version>${dl4j.version}</version></dependency>

我们不会创建神经网络,但为了体验如何使用 DeepLearning4J 创建神经网络,您可以查看 DeepLearning4J 示例( https://github.com/eclipse/deeplearning4j-examples ),例如 LeNet 版本(第一个 CNN 架构),该版本将用于本章的第一个 JavaFX 应用程序:

MultiLayerConfiguration conf = new NeuralNetConfiguration.Builder().seed(seed).activation(Activation.IDENTITY).weightInit(WeightInit.XAVIER).optimizationAlgo(OptimizationAlgorithm.STOCHASTIC_GRADIEN T_DESCENT).updater(updater).cacheMode(cacheMode).trainingWorkspaceMode(workspaceMode).inferenceWorkspaceMode(workspaceMode).cudnnAlgoMode(cudnnAlgoMode).convolutionMode(ConvolutionMode.Same).list()// block 1.layer(0, new ConvolutionLayer.Builder(new int[] {5, 5}, new int[] {1, 1}).name("cnn1").nIn(inputShape[0]).nOut(20).activation(Activation.RELU).build()).layer(1, new SubsamplingLayer.Builder(SubsamplingLayer.PoolingType.MAX, new int[] {2, 2},new int[] {2, }).name("maxpool1").build())// block 2.layer(2, new ConvolutionLayer.Builder(new int[] {5, 5}, new int[] {1, 1}).name("cnn2").nOut(50).activation(Activation.RELU).build()).layer(3, new SubsamplingLayer.Builder(SubsamplingLayer.PoolingType.MAX, new int[] {2, 2},new int[] {2, 2}).name("maxpool2").build())// fully connected.layer(4, new DenseLayer.Builder().name("ffn1").activation(Activation.RELU).nOut(500).build())// output.layer(5, new OutputLayer.Builder(LossFunctions.LossFunction.MCXENT).name("output").nOut(numClasses).activation(Activation.SOFTMAX) // radial basis function required.build()).setInputType(InputType.convolutionalFlat(inputShape[2], inputShape[1], inputShape[0])).build();

从 JavaFX 应用程序训练神经网络

如果你没有训练过的模型,你可以自己训练。这将需要数据,大量的数据,以及神经网络参数和架构的知识。为了交互和可视化神经网络训练过程的进度,我们将使用 JavaFX 应用程序。

为了演示如何从 JavaFX 训练神经网络,我们创建了一个具有以下特性的小应用程序:

  • 查看培训和测试的进度。这可能需要几个月、几天或几个小时。在我们的案例中,我们将进行一个快速培训,这将需要几个小时。您可以在 JavaFX 应用程序中跟踪进度。

  • 能够调整一些超参数:时期数、迭代次数和批量大小。还要为训练和测试输入图像文件以及图像信息选择路径。

  • 训练后导出模型,并导入要训练的模型配置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-4

一个 JavaFX 应用程序,用于可视化神经网络训练过程的进度

图 13-4 所示的应用程序是使用本书中已经讨论过的控件构建的。图表还用于显示神经网络“学习”的进度,当该过程完成时,您可以将现在训练好的神经网络保存到您的磁盘上。导出的模型可用于数据新实例的真实预测。

为了探索完整的代码,您可以查看类 TrainingHelperApp(可在github.com/Apress/definitive-guide-modern-java-clients-javafx17访问)。这里我们将重点介绍 JavaFX 如何访问 DL4J APIs。用于训练的 DL4J 基础模型被封装在 NeuralNetModel 接口中,使用 Java 服务提供者接口实现该接口提供定制模型是可能的。默认情况下,我们有一个基于 LeNet 的内置 DL4J 模型,LeNet 是 Yann LeCun 创建的第一个卷积神经网络。神经网络类型为 org . deep learning 4j . nn . multilayer . multilayer network . DL4J 也提供计算图。对于这个例子,让我们保持多层网络:

import org.deeplearning4j.nn.multilayer.MultiLayerNetwork;
public interface NeuralNetModel {public String getId();public MultiLayerNetwork getModel(int[] inputShape, int numClasses);
}

组合框中填充了 NeuralNetModel 的可用实现,实际的模型是使用 getModel 方法访问的。在运行培训流程之前,用户必须选择培训和测试目录。目录应该有一个结构,其中图像位于与其类别相对应的文件夹下,例如,猫图像必须位于名为 cat 的文件夹中。当单击按钮 Run 时,将检索所有输入的信息,然后将其传递给 prepareForTraining 方法:

    private void prepareForTraining(String modelId, int[] inputShape, int epochs, int batchSize, File trainingDir, File testingDir) {status("Preparing for training...");runningProperty.set(true);try {DataSetIterator trainingIterator = DL4JHelper.createIterator(trainingDir, inputShape[1], inputShape[2], inputShape[0], batchSize, SEED);DataSetIterator testingIterator = DL4JHelper.createIterator(testingDir, inputShape[1], inputShape[2], inputShape[0], batchSize, SEED);var currentModel = getNeuralNetById(modelId).getModel(inputShape, trainingIterator.getLabels().size());lastModel.set(currentModel);currentModel.setListeners(new AsyncScoreIterationListener(this::updateScore));clearSeries();launchTrainingThread(epochs, trainingIterator, testingIterator);} catch (IOException e) {e.printStackTrace();}}

在 prepareForTraining 中,用户选择的目录用于创建 DataSetIterator。DL4J 为我们提供了一个迭代器 API,使得加载外部文件以输入神经网络变得容易。在我们的例子中,我们基于图像文件创建一个迭代器,标签是基于给定图像文件的父路径生成的。它负责为我们处理一切艰难的工作;否则,我们必须将图像加载到一个数组中,以便输入到神经网络中。创建的迭代器还提供了关于我们的数据集中有多少标签(或类)的信息,我们使用输入的输入形状和神经网络模型 ID 来检索实际的模型。在此之后,我们注册一个侦听器,每次更新分数时都会调用它,我们使用一个方法引用来更新分数以注册它,最后我们启动调用 launchTrainingThread 的训练过程,传递我们创建的历元和迭代器的数量:

    private void launchTrainingThread(int epochs, DataSetIterator trainingIterator, DataSetIterator testingIterator) {var currentModel = lastModel.get();new Thread(() -> {var result  = "";int epochNum = 0;for (int i = 0; i < epochs; i++) {epochNum = (i +1);currentModel.fit(trainingIterator);status("Evaluating...");Evaluation eval = currentModel.evaluate(testingIterator);double progress = (double) i / (double) epochs;var accuracy =  eval.accuracy();var precision = eval.precision();var f1 = eval.f1();updateSeries(accuracySeries, epochNum, accuracy);updateSeries(precisionSeries, epochNum, precision);updateSeries(f1Series, epochNum, f1);testingIterator.reset();trainingIterator.reset();result = "( A: " + evalutionFormat.format(accuracy)  +", P: " + evalutionFormat.format(precision) +", F1:" + evalutionFormat.format(f1) + " )";if (stopRequested) {status("Stop Requested on epoch "  + epochNum + ".                         Results: " + result);stopRequested = false;break;} else {status("Epoch " + epochNum  + "/" + epochs + " " +                         result);setProgress(progress);}}status("Process stoped at epoch " + epochNum  + ".Results: " + result);Platform.runLater(() -> runningProperty.set(false));}).start();}

在这个方法中,我们开始获取用户选择的模型;然后,我们启动一个包含执行训练的过程的线程。在不同的线程中这样做的原因是为了避免锁定 JavaFX 线程;这样,如果我们认为我们已经达到了好的结果,我们就可以停止这个过程。训练过程基本上是拟合模型中的迭代器,对模型进行评估。模型评估返回常用的度量来查看模型有多好:准确度、精确度和 f1 分数。每个指标都有一个相应的图表 XYSeries,在培训过程中会更新。一切都发生在历元时间,因此在从 0 到历元数的 for 循环中;但是,如果用户在应用程序运行时单击 Run 按钮,那么 stopRequested 标志就会变为 true,流程就会停止并允许用户导出模型。

您可能已经注意到,在这个方法上,我们不直接与 JavaFX 控件交互;相反,我们必须调用 status、updateSeries 和 setProcess。这些方法更新 JavaFX 线程中与 JavaFX 相关的类。请参见以下内容:

    private void setProgress(final double progress) {Platform.runLater(() ->  progressProperty.set(progress));}private void updateScore(Integer i, Double d) {updateSeries(scoreSeries, i, d);}private void updateSeries(XYChart.Series<Number, Number> series, Integer i, Double d) {Platform.runLater(() -> series.getData().add(new XYChart.Data<>(i, d)));}private void status(String txt) {Platform.runLater(() ->  txtProgress.set(txt));}

当训练过程结束或停止时,可以导出现在已训练的模型,这意味着它将调整权重并准备好预测新数据。这可以在方法 exportModel 上简单地完成:

    private void exportModel(ActionEvent event) {var source = (Button) event.getSource();var modelOutputFile = fileChooser.showSaveDialog(source.getScene().getWindow());if (modelOutputFile != null) {try {ModelSerializer.writeModel(lastModel.get(), modelOutputFile, true);status("Model saved to " + modelOutputFile.getAbsolutePath());} catch (IOException e1) {e1.printStackTrace();}}}

将图像从 JavaFX 读入神经网络

您可能会遇到这样的情况:您必须从 JavaFX 应用程序内部获取内容,然后输入到神经网络中以使用其输出。比如在一个游戏中,你想把实际的屏幕传递给一个神经网络来调整游戏参数;或者,如果您正在运行模拟,您可以将模拟的状态输入到神经网络中进行实时预测。在这些情况下,我们需要知道如何从 JavaFX 获得快照,以输入到神经网络中。

你可能听说过 Quick,Draw!这是谷歌的一个在线工具,可以猜测你在画什么。谷歌把这个开放给每个人来玩这个工具,还存储了所有的图纸,超过了 10 亿张图纸。好消息是他们把数据提供给所有人使用( https://quickdraw.withgoogle.com/data )。

数据有几种不同的二进制格式,有数百个类,每个类有数千幅图像。为了简化训练过程,我们只上动物课(狗、猫等)。)并将它们转换成人类可读的格式 PNG,这种格式也可以从 DL4J 数据集迭代器类中访问。我们还把图像做成黑白的,并把它们的尺寸调整到 28 × 28,这与著名的手写数字数据集 MNIST 的图像尺寸相同。这样我们可以使用 DL4J 示例中的 MNIST 神经网络架构,MnistClassifier ( http://projects.rajivshah.com/blog/2017/07/14/QuickDraw )。我们将使用一个简单的 LeNet 神经网络来预测输入了什么涂鸦;然而,我们将展示的应用程序可以适用于其他神经网络模型。

应用程序可以在一个单独的类 GuessTheAnimal 中找到。运行该应用程序会产生类似于图 13-5 的屏幕截图。第一步是声明包含输入图像大小、模型位置和我们在训练模型时使用的类的常量。在我们的例子中,我们选择了一些动物类。我们使用了一个具有 68%准确率和 15 个类别的模型。您可以使用我们上一节中的应用程序来训练您自己的模型,并构建一个更精确的模型:

    private static final String MODEL_PATH = "/quickdraw-model-15-68.zip";private static final String CLASSES[] = { "Bee", "Bird", "Cat", "Dog", "Duck", "Elephant", "Fish", "Frog", "Horse", "Lion", "Mouse", "Pig", "Rabbit", "Snake", "Spider" };private static final int INPUT_WIDTH = 28;private static final int INPUT_HEIGHT = 28;private static final Double THRESHOLD = 0.1d;

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 13-5

由 LeNet 神经网络分析的图形

该应用程序由两部分组成:加载模型和构建 UI。为了加载模型,我们使用 ModelSerializer 类。注意,您可以使用从我们之前讨论的应用程序中导出的任何模型。只要确保相应地调整常量。加载模型很简单。我们只需要调用 model serializer . restore multilayernetwork 并传递包含模型的文件或输入流。请参见 initModelAndLoader 方法。在此方法中,我们还创建了 NativeImageLoader,它是实际将内容转换为 INDArray 的类,这使它对神经网络非常有用:

    private void initModelAndLoader() throws IOException {model = ModelSerializer.restoreMultiLayerNetwork(GuessTheAnimal.class.getResource AsStream(MODEL_PATH));model.init();loader = new NativeImageLoader(INPUT_WIDTH, INPUT_HEIGHT, 1, true);}

UI 内置在 buildUI()方法中,由三个控件组成:用于接收用户绘图的画布、触发预测过程的按钮和包含输出的标签。当用户在画布上拖动鼠标时,应用程序会画出小圆圈,给人一种铅笔在纸上写字的感觉。右键单击清除画布和结果标签:

private StackPane buildUI() {var canvas = new Canvas(APP_WIDTH, APP_HEIGHT);var btnGuess = new Button("Guess!");var lblOutput = new Label("");var root = new StackPane(canvas, btnGuess, lblOutput);lblOutput.setTextFill(Color.RED);txtOutput = lblOutput.textProperty();ctx = canvas.getGraphicsContext2D();ctx.setLineWidth(30);canvas.setOnMouseDragged(e -> {ctx.setFill(Color.BLACK);ctx.fillOval(e.getX() - 15, e.getY() - 15, 30, 30);});canvas.setOnMouseClicked(e -> {if (e.getButton() == MouseButton.SECONDARY) {clearCanvas();}});btnGuess.setOnAction(evt -> {var predictions = predictCanvasContent();var pairs = sortAndMap(predictions);txtOutput.set(pairs.toString());});StackPane.setAlignment(btnGuess, Pos.BOTTOM_LEFT);StackPane.setAlignment(lblOutput, Pos.BOTTOM_RIGHT);return root;
}

此代码最重要的部分是我们获取画布内容并将其转换为 INDArray,以便可以输入到神经网络模型中。这是在方法 predictCanvasContent 和 getScaledImage 中完成的。在 getScaledImage 上,我们将画布的屏幕截图转换为 WritableImage,使用 SwingFXUtils 将其转换为 java.awt.Image,最后将其写入 BufferedImage,buffered image 也会将图像缩放到与神经网络模型中使用的图像相同的大小。我们还将最后预测的图像保存到外部文件中;它对调试很有用。在 predictCanvasContent 中,我们将 scaledImage 转换为 INDArray,然后输入到神经网络模型中。模型本身返回一个包含 1 个× 15 个位置的 INDArray 因此,在预测之后,我们将其转换为地图,并过滤结果低于我们定义为常量(默认为 0.1)的阈值的结果:

private Map<String, Double> predictCanvasContent() {try {var img = getScaledImage();INDArray image = loader.asRowVector(img);INDArray output = model.output(image);double[] doubleVector = output.toDoubleVector();var results = new HashMap<String, Double>();for (int i = 0; i < doubleVector.length; i++) {results.put(CLASSES[i], doubleVector[i]);}return results.entrySet().stream().filter(e -> e.getValue() > THRESHOLD).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));} catch (Exception e) {throw new RuntimeException(e);}
}
private BufferedImage getScaledImage() {var canvas = ctx.getCanvas();WritableImage writableImage = new WritableImage((int) canvas.getWidth(), (int) canvas.getHeight());canvas.snapshot(null, writableImage);Image tmp = SwingFXUtils.fromFXImage(writableImage, null).getScaledInstance(INPUT_WIDTH, INPUT_HEIGHT, Image.SCALE_SMOOTH);BufferedImage scaledImg = new BufferedImage(INPUT_WIDTH, INPUT_HEIGHT, BufferedImage.TYPE_BYTE_GRAY);Graphics graphics = scaledImg.getGraphics();graphics.drawImage(tmp, 0, 0, null);graphics.dispose();try {File outputfile = new File("last_predicted_image.jpg");ImageIO.write(scaledImg, "jpg", outputfile);} catch (IOException e) {e.printStackTrace();}return scaledImg;
}

检测视频中的对象

对于下一个应用,我们将探索一个叫做 YOLO 的神经网络模型架构(你只看一次)。关于 YOLO 如何工作的总结可以在它的论文中找到: 1

我们将对象检测重新定义为一个单一的回归问题,直接从图像像素到边界框坐标和类别概率。使用我们的系统,你只需要看一次(YOLO)图像就可以预测物体的存在和位置。

本文还提供了以下简化其工作方式的图片。

原文是 2016 年的。后来,更多的论文被引入。最新的是给 YOLO3 的,更精准更快。训练一个 YOLO 神经网络需要比平常更大的图像;第一个 YOLO 版本的输入大小是 448 × 448,这意味着在个人电脑上需要很长时间。幸运的是,DL4J 提供了 TinyYOLO 和 YOLO2,随时可供我们使用。在本节中,我们将探索一个 JavaFX 应用程序,用于检测正在播放的视频中的对象。

应用程序开始声明一些对应用程序很重要的常量。让我们看看每个常量:

  • APP_WIDTH 和 APP_HEIGHT:实际的图像大小。

  • TARGET_VIDEO:支持的视频文件的 URL。如果它在类路径中,可以直接使用它的路径;不过 JavaFX 也支持 URL 协议,所以可以使用协议 file:/{path to file}加载本地文件。

  • 阈值:切割值。检测到的值小于阈值的对象不会出现在神经网络输出中。

  • 标签:用于训练神经网络模型的标签。默认情况下,它具有用于训练 DL4J 默认模型的类,但是可以将其更改为自定义 YOLO 模型。

  • 输入宽度、输入高度、输入通道:YOLO 神经网络使用的输入图像信息。它还使用 YOLO2 DL4J 型号的值。

  • GRID_W 和 GRID_H:将原始图像划分为一个网格,输出检测到的物体位置与这个网格相关;因此,在计算输出框时,需要使用网格大小。可以使用 org . deep learning 4j . zoo . model . helper . darknet helper getGridWidth 和 getGridHeight 方法进行计算。

  • 每秒帧数:每秒扫描的帧数。如果更高,处理时间会更长,但检测到的对象高光看起来会更精确。

    private static final double APP_WIDTH = 800;private static final double APP_HEIGHT = 600;private static final String TARGET_VIDEO = "/path/to/video";private static final double THRESHOLD = 0.65d;private static final String[] LABELS = { "person", "bicycle", "car", "motorbike", "aeroplane", "bus", "train", "truck", "boat", "traffic light", "fire hydrant", "stop sign", "parking meter", "bench", "bird", "cat", "dog", "horse", "sheep", "cow", "elephant", "bear", "zebra", "giraffe", "backpack", "umbrella", "handbag", "tie", "suitcase", "frisbee", "skis", "snowboard", "sports ball", "kite", "baseball bat", "baseball glove", "skateboard", "surfboard", "tennis racket", "bottle", "wine glass", "cup", "fork", "knife", "spoon", "bowl", "banana", "apple", "sandwich", "orange", "broccoli", "carrot", "hot dog", "pizza", "donut", "cake", "chair", "sofa", "pottedplant", "bed", "diningtable", "toilet", "tvmonitor", "laptop", "mouse", "remote", "keyboard", "cell phone", "microwave", "oven", "toaster", "sink", "refrigerator", "book", "clock", "vase", "scissors", "teddy bear", "hair drier", "toothbrush" };private final int INPUT_WIDTH = 608;private final int INPUT_HEIGHT = 608;private final int INPUT_CHANNELS = 3;private final int GRID_W = INPUT_WIDTH / 32;private final int GRID_H = INPUT_HEIGHT / 32;private final double FRAMES_PER_SECOND = 20d;

应用程序 UI 由一个用于视频回放的媒体视图、一个包含突出显示检测到的对象的矩形的窗格和一个显示正在运行的任务执行进度的标签组成。所有东西都堆放在 StackPane 上,标签朝向底部。这些都是在 start 方法中完成的,但是在构建 UI 之前,我们为每个矩形生成颜色,这些颜色将突出显示检测到的对象并初始化 YOLO2 模型。注意,第一次执行这段代码时,它将下载预训练的模型;因此,这可能需要一段时间:

for (int i = 0; i < LABELS.length; i++) {colors.put(LABELS[i], Color.hsb((i + 1) * 20, 0.5, 1.0));
}
var yoloModel = (ComputationGraph)  YOLO2.builder().build().initPretrained();
String videoPath = DetectObjectsInVideoImproved.class.getResource(TARGET_VIDEO).toString();
imageLoader = new NativeImageLoader(INPUT_WIDTH, INPUT_HEIGHT, INPUT_CHANNELS,new ColorConversionTransform(COLOR_BGR2RGB));
var media = new Media(videoPath);
var mp = new MediaPlayer(media);
var view = new MediaView(mp);
Label lblProgress = new Label();
lblProgress.setTextFill(Color.LIGHTGRAY);
view.setFitWidth(APP_WIDTH);
view.setFitHeight(APP_HEIGHT);
view.setPreserveRatio(false);
pane = new Pane();
pane.setMinWidth(APP_WIDTH);
pane.setMinHeight(APP_HEIGHT);
var root = new StackPane(view, pane, lblProgress);
StackPane.setAlignment(lblProgress, Pos.BOTTOM_CENTER);
stage.setScene(new Scene(root, APP_WIDTH, APP_HEIGHT));
stage.show();
stage.setTitle("Detect Objects");

该应用程序还允许用户通过单击来暂停视频,由于我们在媒体视图的顶部有一个窗格,所以我们在窗格上而不是在媒体视图上注册鼠标监听器:

        pane.setOnMouseClicked(e -> {if (mp.getStatus() == Status.PLAYING) {mp.pause();} else if (mp.getStatus() == Status.PAUSED) {mp.play();} else if (mp.getStatus() == Status.STOPPED) {mp.seek(mp.getStartTime());mp.play();}});mp.setOnEndOfMedia(() -> {mp.stop();pane.getChildren().forEach(c -> c.setVisible(false));});

预测不是实时进行的。原因是在没有 GPU 处理的机器中,单个预测几乎需要 500 ms。当拥有 GPU 时,预测过程的一部分将由多个 GPU 核心来完成,从而使其速度更快。YOLO 的论文谈到每秒 155 帧;然而,拍摄 JavaFX 节点的快照将很难实现这一结果,但是在这个应用程序中,使用快照是因为您可以预处理媒体视图节点,然后运行 YOLO(例如,缩放或应用效果),将 JavaFX 的功能与 YOLO 结合起来。此外,你可能不想运行 YOLO 的视频。任何 JavaFX 节点都可以成为 YOLO 预测的对象,因此它有更多的可能性。

在我们的例子中,技巧不是实时进行预测,而是在应用程序运行时收集帧并安排预测任务,创建包含检测到的对象高亮显示的 JavaFX 节点,然后在视频顶部显示它。注意,一旦创建了组,我们就给它一个 ID,这样我们就可以根据为用户显示的当前框架隐藏或显示它。我们跟踪每个任务以避免多余的执行,只有在任务完成后,带有检测到的对象的组才会添加到窗格中(请参见 target.setOnSucceeded)。换句话说,视频至少需要播放一次,以便收集所有帧并安排处理。在 trackTasks 中跟踪预定的任务,一旦给定帧 ID 的任务完成,我们将高亮显示包含检测到的对象的组,并隐藏其他对象。一切都是在一个监听器中完成的,这个监听器附加到媒体播放器播放的当前时间,所以它只在视频播放的时候被调用;否则,它不会收集帧进行处理:

        var finishedTasks = new AtomicInteger();var previousFrame = new AtomicLong(-1);mp.currentTimeProperty().addListener((obs, o, n) -> {if(n.toMillis() < 50d) return;Long millis = Math.round(n.toMillis() / (1000d / FRAMES_PER_SECOND));final var nodeId = millis.toString();if(millis  == previousFrame.get()) {return;}previousFrame.set(millis);trackTasks.computeIfAbsent(nodeId, v -> {var scaledImage = getScaledImage(view);PredictFrameTask target = new PredictFrameTask(yoloModel, scaledImage);target.setOnSucceeded(e -> {var detectedObjectGroup = getNodesForTask(nodeId, target);Platform.runLater(() -> pane.getChildren().add(detectedObjectGroup));updateProgress(lblProgress, trackTasks.size(), finishedTasks.incrementAndGet());});Thread thread = new Thread(target);thread.setDaemon(true);thread.start();return true;});updateProgress(lblProgress, trackTasks.size(),finishedTasks.get());pane.getChildren().forEach(node -> node.setVisible(false));Optional.ofNullable(pane.lookup("#" + nodeId)).ifPresent(node -> node.setVisible(true));});}

既然您已经了解了我们是如何收集帧并处理它们的,那么让我们来考虑一下实际的帧处理。在调度任务之前,有一个对 getScaledImage 的调用,其结果将被传递给任务。这个方法获得媒体视图的快照,就像我们以前做的一样,但是这次我们得到的不是黑白图像,而是彩色图像。YOLO 输入图像使用三个通道,每种颜色一个通道(红色、绿色和蓝色):

    private BufferedImage getScaledImage(Node targetNode) {writableImage = new WritableImage((int) targetNode.getBoundsInLocal().getWidth(), (int) targetNode.getBoundsInLocal().getHeight());targetNode.snapshot(null, writableImage);Image tmp = SwingFXUtils.fromFXImage(writableImage, null).getScaledInstance(INPUT_WIDTH, INPUT_HEIGHT, Image.SCALE_SMOOTH);BufferedImage scaledImg = new BufferedImage(INPUT_WIDTH, INPUT_HEIGHT, BufferedImage.TYPE_INT_RGB);Graphics graphics = scaledImg.getGraphics();graphics.drawImage(tmp, 0, 0, null);graphics.dispose();return scaledImg;}

在 setOnSucceeded 侦听器上,我们将处理任务返回的检测到的对象。每个对象具有每个检测到的矩形的初始点以及其他信息,但是只需要点 x 来构建矩形以突出检测到的对象。然而,在创建矩形之前,需要转换坐标。首先,我们需要弄清楚是否需要缩放,因为应用程序大小可能大于图像输入,并且所有坐标都是相对于输入的,然后计算原始图像上的实际坐标,因为所有坐标都是相对于网格的,最后计算矩形的宽度和高度。除了矩形之外,我们还添加了一个标签来显示预测的类,它被添加到一个组中,因此我们可以唯一地处理标签和矩形。对每个检测到的对象都这样做:

   private Group getNodesForTask(final String nodeId, PredictFrameTask    target) {try {var predictedObjects = target.get();var detectedObjectGroup = getPredictionNodes(predictedObjects);detectedObjectGroup.setId(nodeId);detectedObjectGroup.setVisible(false);return detectedObjectGroup;} catch (Exception e) {throw new RuntimeException(e);}}private Group getPredictionNodes(List<DetectedObject> objs) {Group grpObject = new Group();objs.stream().map(this::createNodesForDetectedObject).flatMap(l -> l.stream()).forEach(grpObject.getChildren()::add);return grpObject;}private List<Node> createNodesForDetectedObject(DetectedObject obj) {double[] xy1 = obj.getTopLeftXY();double[] xy2 = obj.getBottomRightXY();var w  = INPUT_WIDTH;var h  = INPUT_HEIGHT;var wScale  = (APP_WIDTH / w);var hScale  = (APP_HEIGHT / h);var x1 = (w ∗ xy1[0] / GRID_W) * wScale;var y1 = (h ∗ xy1[1] / GRID_H) * hScale;var x2 = (w ∗ xy2[0] / GRID_W) * wScale;var y2 = (h ∗ xy2[1] / GRID_H) * hScale;var rectW = x2 - x1;var rectH = y2 - y1;var label = LABELS[obj.getPredictedClass()];Rectangle rect = new Rectangle(x1, y1, rectW, rectH);rect.setFill(Color.TRANSPARENT);Color color = colors.get(label);rect.setStroke(color);rect.setStrokeWidth(2);Label lbl = new Label(label);lbl.setTranslateX(x1 + 2);lbl.setTranslateY(y1 + 2);lbl.setTextFill(color);lbl.setFont(Font.font(Font.getDefault().getFamily(), FontWeight.EXTRA_BOLD, FontPosture.ITALIC, 10));return List.of(rect, lbl);}

最后,我们有一个任务,它实现了 javafx.concurrent.Task 并为它提供了一个 List 类型。JavaFX 并发任务允许我们运行一些繁重的操作,然后检索结果,而不占用 JavaFX 主线程。这个预测几乎和我们快速绘制中所做的一样!例子。主要区别在于,为了提取对象,现在我们使用 org . deep learning 4j . nn . layers . obj detect . yolo 2 output layer 类中的一个实用程序方法来从 Array:

   public class PredictFrameTask extends Task<List<DetectedObject>> {ComputationGraph yoloModel;BufferedImage scaledImage;public PredictFrameTask(ComputationGraph yoloModel, BufferedImage scaledImage) {this.yoloModel = yoloModel;this.scaledImage = scaledImage;}@Overrideprotected List<DetectedObject> call() throws Exception {return predictObjects();}private List<DetectedObject> predictObjects() {org.deeplearning4j.nn.layers.objdetect.Yolo2OutputLayer yout =(org.deeplearning4j.nn.layers.objdetect.Yolo2OutputLayer)yoloModel.getOutputLayer(0);try {var imgMatrix = imageLoader.asMatrix(scaledImage);var scaler = new ImagePreProcessingScaler(0, 1);scaler.transform(imgMatrix);INDArray output = yoloModel.outputSingle(imgMatrix);return yout.getPredictedObjects(output, THRESHOLD);} catch (IOException e) {throw new RuntimeException(e);}}

的结果中获取预测的对象

这是构建您自己的应用程序的一个非常简单的起点。考虑构建自己的 YOLO 驱动的应用程序的可能性,如在一个属性中寻找入侵者,计算街道上的汽车数量,在大图像中寻找对象,等等!

《你只看一次:统一的实时物体检测》,作者:约瑟夫·雷德蒙、桑托什·迪夫瓦拉、罗斯·吉斯克和阿里·法尔哈迪( https://arxiv.org/pdf/1506.02640.pdf )。

十四、使用 JavaFX 的科学应用

由 Johan Vos 撰写

最近取得的许多惊人进展都是在数据科学领域,或者至少是与之相关的领域。不同形式的机器学习、大数据计算和量子计算正迅速对整个社会产生越来越大的影响。

这些进步的技术基础是基于不同的科学研究领域。通常情况下,技术基础是与实现无关的。不同的语言、平台和二进制文件可以用来实现技术基础。

在这一章中,我们将解释为什么 Java,特别是客户机 Java(包括 JavaFX ),是那些想用他们掌握的语言创建科学应用程序的开发人员的最佳选择。

我们将首先演示几个真实的示例,然后解释一种更通用的方法,它允许您用 Java 创建科学应用程序,包括进行与科学应用程序相关的研究工作。

用于太空探索的 JavaFX

使用 JavaFX 技术的一个伟大的科学应用是深空轨道探测器,或 DSTE。这是由 a.i. solutions 创造的产品,由 NASA 使用。该产品在位于 https://ai-solutions.com/dste 的 a.i. solutions 网站上有所描述。

据该网站介绍,深空轨道探索者是一个交互式软件包,结合了尖端的多体轨道设计技术和创新的可视化技术,以大幅减少轨道设计所花的时间。

通过 DSTE,用户可以设计空间物体的轨迹,这通常需要大量的计算。通常,在计算完成之前,这些计算是在没有任何可视化或交互性的情况下完成的。DSTE 的产品使设计过程更加“敏捷”和互动。

DSTE 的主要好处之一是它允许在任务设计过程中使用交互式可视化。这样,选择满足特定任务约束的轨道变得更加容易和直观。

结合高性能计算和复杂可视化的需求是 Java 和 JavaFX 的一个很好的用例。Java 本身是非常可扩展的,有大量强大的 Java APIs 和框架可以帮助开发人员和操作人员在多核环境中扩展 Java 代码。JavaFX 平台允许包括 3D 模型和画布渲染在内的可视化,从而使用硬件加速渲染,利用 GPU 的可用性。由于 JavaFX 是纯 Java,这种呈现可以很容易地与负责高性能计算的代码片段集成。

Deep Space Trajectory Explorer 以多种方式利用 JavaFX 平台提供的性能。它包含 2D 和 3D 的不同视图。许多视图都支持点击-拖动功能。画布组件用于在屏幕上呈现数百万个链接的数据点,而不会冻结布局。过滤器允许选择和取消选择许多选项,所产生的变化实时呈现在视图中。

图 14-1 1 显示了由 DSTE 产品生成的屏幕截图,显示了该工具提供的一些视图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14-1

DSTE 生成的截图

深空轨道探测器不是一个容易创建的应用程序。它需要深入了解轨道背后的物理学、高性能计算和 UI 开发。JavaFX 是整个解决方案中的关键组件,证明了该平台的强大。

用于量子计算的 JavaFX

量子计算正在科学环境和 IT 部门中迅速获得兴趣。

量子计算的前景之一是,一些用经典计算机极难解决或实际上不可能解决的问题,可以用量子计算机轻松解决。尤其是在算法表现出指数级时间行为的领域,量子计算机可以大有作为。

量子计算机使用一些从根本上存在于自然界的核心概念,但在经典计算机中没有。

在经典计算机中,最细粒度的单位是位。一位不是 0 就是 1。在量子计算机中,最精细的单位是量子位。一个量子位可以保存值 0 或 1,但它也可以处于所谓的叠加态,在这种状态下,它保存 0 和 1 的线性组合。然而在测量时,一个量子位总是返回 0 或 1。因此,量子计算的算法必须利用叠加态,而不需要在处理过程中测量量子位。

在经典计算中,比特是由门操纵的。例如,非门将翻转一位的值。当该位在进入门之前为 0 时,门之后的结果将为 1,反之亦然。

同样,在量子计算中,量子位是由量子门操纵的。

虽然有一些非常早期的量子计算机实验芯片可用,但量子计算还没有为主流开发做好准备。制造一台具有足够量子位的量子计算机,并使其在合理的时间内保持可用的实际需求是巨大的。因此,只有少数几个原型具有有限数量的量子位。

然而,由于量子计算的巨大潜在影响,许多开发人员已经在研究可能受益于量子计算的算法。通常,本地或基于云的模拟器用于开发这些算法,一些公司现在开始提供真正的量子计算机作为云服务。

这些算法通常用编程语言开发,并使用电路可视化来可视化。

其中一个量子模拟器 Strange 正在使用一个名为 StrangeFX 的配套工具,该工具是使用 JavaFX 构建的,用于呈现电路。StrangeFX 可以在 GitHub 的 https://github.com/redfx-quantum/strangefx 获得。

StrangeFX 允许开发人员将量子门拖到量子位线上。当他们这样做时,本地仿真器评估电路并实时显示结果。JavaFX 的 draganddrop 功能为开发量子电路提供了一种非常直观的方式,并且与量子模拟算法的集成非常简单,因为两个组件都是用 Java 编写的。

图 14-2 显示了 StrangeFX 的一个简单截图,显示了三个量子位和工具栏中的一些门,可以拖动到量子位线上。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14-2

StrangeFX 允许开发人员将量子门拖到量子位线上

一个更复杂的例子包括图 14-3 所示的 Grover 搜索的模拟,这是一个著名的量子算法。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14-3

格罗弗氏 sarch 的模拟

使用 JShell

在不久前,我们看到了两个不同领域的活动之间的断层线:一方面是使用编程语言创建应用程序的开发人员,另一方面是从事基础设施和运营工作的人员。开发人员主要在一个孤立的环境中为特定的问题开发业务解决方案。一旦问题得到解决,解决方案就被交给 it 部门,IT 部门必须将其投入生产。这两个世界之间的差距导致了许多与可伸缩性、文档、版本、责任、依赖性等相关的问题。“它为我工作”的情况经常阻碍了从创建业务解决方案的开发人员到将解决方案投入大规模生产的运营人员的过渡。

这种差距现在通常通过所谓的“DevOps”方法来解决,其中开发和运营的一些重叠部分被集中在一个方法或团队中。如图 14-4 所示,包括容器化在内的许多软件改进使得开发和运营能够更加紧密地合作。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14-4

DevOps 方法

开发和运营的重叠部分在跨域 DevOps 环境中解决。

如今,人们对数据科学的兴趣越来越浓厚,这表明两个群体之间出现了一条新的断层线:从事研究的人和从事生产开发的开发人员。通常情况下,这项研究是由那些有极其困难和复杂的科学问题需要解决的科学家来完成的。那些研究人员应该关注核心问题,而不是特定编程语言的语法或特定行为。因此,像 Matlab 和 Python 这样伟大的科学平台或语言经常被科学家用来解决这些核心问题。

然而,一旦核心问题得到解决,它通常需要集成到产品中并投入生产。这经常会产生新的问题。科学平台专注于帮助研究人员找到科学问题的最佳解决方案,而不是找到与数据库、web 服务以及高可用性和安全性服务集成的最佳方式。

后者是 Java 擅长的领域。然而,当科学家不得不使用与 Java 企业开发人员相同的环境时,他们的生产力很可能会下降。

当进行纯研究时,迭代周期与开发业务应用程序和运行集成测试非常不同。在研究过程中,科学家希望衡量改变单个变量的影响,或者他们希望在现有的深度学习模型中添加一个新的层。他们应该能够检查他们算法的参数和中间值。这不同于调试业务应用程序。它需要与算法本身进行更快、更深入的交互,而不需要重新编译应用程序或运行单元测试。

在下文中,我们将展示由于 JShell,现代 Java 是如何支持这种快速的科学发展的。从 Java 9 开始,JShell 工具包含在 Java SE 发行版中。这是一个所谓的 REPL,是“读取-评估-打印-循环”的缩写,它为开发人员提供了一个简单的交互式环境,用于创建和检查应用程序和算法。JShell 构建在 Java 之上,您可以利用 JShell 的所有 Java APIs,包括 JavaFX APIs。

因此,JShell 是一个很好的工具,允许在科学研究和生产开发之间进行转换,如图 14-5 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14-5

JShell 允许科学开发和业务环境集成之间的过渡

我们将首先展示 JShell 的基本功能。接下来,我们将展示如何使用 ND4J 库在 JShell 中轻松处理线性代数。最后,我们将演示如何使用 JavaFX 和 JShell 在原型开发过程中实现真正简单快速的可视化。

使用 JShell

启动 JShell 非常容易。JShell 是一个工具,与 javac 和 java 包装器位于同一个目录中。因此,如果您设法将 java 和 javac 添加到您的路径中,只需调用

jshell

应该启动工具。一旦 JShell 启动,您就进入了 JShell 环境,您可以在其中输入命令或语句。

启动 JShell 后,您会看到以下内容:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

请注意,版本号显示为“17-内部”这样做的原因是这个截图(以及接下来的截图)是在二进制版本可用之前用 OpenJDK 17 的定制版本创建的。

JShell 允许您创建 Java 语句。例如,可以在 JShell 提示符后编写以下语句:

System.out.println("Hello, JShell");

它将立即导致以下响应:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

虽然这看起来与创建常规 Java 应用程序的方式非常相似,但重要的是要注意,我们不必创建包、类或 main 方法。以下语句等效于创建包含以下定义的类 HelloWorld:

public class HelloWorld {public static void main(String[] args) {System.out.println("Hello, Jshell");}
}

接下来,我们必须编译这个类

javac HelloWorld.java

并使用运行它

java HelloWorld

Note

从 Java 11 开始,可以跳过编译步骤,直接使用java HelloWorld.java运行类。

虽然最终结果是相同的(“Hello,JShell”正在打印),但是步骤却非常不同。当处理复杂的或者模块化的软件时,创建一个带有修饰符的类和方法是非常重要的,在这种情况下不应该被看作是开销。然而,当我们只是想知道前面的代码片段中会打印出什么内容时,使用 JShell 的方法给了我们更快的答案。

使用 JShell 打印“Hello,JShell”并不是很有野心,但是我们展示了您可以在 Java 应用程序和 JShell 中使用相同的 Java 语法。因此,熟悉 Java 是 JShell 的关键优势之一。

科学应用通常需要数学运算。我们现在将展示一个具有更多数学功能的示例。

以下代码片段将打印取值为 0、30、60 和 90 度的参数的正弦值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

同样,请注意这个代码片段与实现相同功能的 Java 应用程序是多么相似。为了方便起见,这样的 Java 应用程序如下所示:

import java.util.stream.*;
public class JshellSin {public static void main(String[] args) {IntStream.range(0, 4).mapToDouble(i -> 30\. * i * 2* Math.PI/360.).forEach(d -> System.out.println(Math.sin(d)));}
}

JShell 的一大优点是集成的编辑器功能。我们可以使用向上/向下箭头移动到上一条/下一条语句并编辑该语句。

例如,假设我们在算法中犯了一个错误。我们想打印余弦,而不是正弦。

我们可以很容易地做到这一点,按一次向上箭头键,这将再次显示前一行,我们可以修改它,以便我们用“Math.cos”替换“Math.sin”

按 return 键会立即重新计算表达式并打印结果:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

因此,JShell 允许我们通过编辑语句并立即获得关于结果的反馈来试验我们的语句或算法。

这种方法已经更类似于科学家如何使用 Python 和 Matlab 来创建算法。

JShell 的一个巨大优势是代码与新的或现有的 Java 应用程序 100%兼容。JShell 代码可以很容易地粘贴到 Java 类中。

JShell 包含许多用于保存和加载代码片段的命令。然后可以将这些片段粘贴到您的应用程序中。

因此,由科学家使用 JShell 完成的最终成果,可以立即被使用 IDE 并将科学算法与项目中的其他组件集成在一起的 Java 开发人员使用。JShell 片段可以封装在一个私有方法中,提交给一个执行程序,周围是头参数,包括安全凭证等等。

虽然我们触及了 JShell 的一些核心概念,但是我们仅仅触及了可能的表面。在本章的剩余部分,我们将关注如何在结合科学工作和高质量可视化的环境中使用 JShell。如果你想了解更多关于 JShell 本身的知识,建议你去 https://docs.oracle.com/en/java/javase/17/jshell/introduction-jshell.html 看看 JShell 官方产品页面。

关于 ND4J

在前一章中介绍的 ND4J 线性代数库允许 Java 开发人员以一种对 Java 开发人员非常方便的方式访问高性能的线性代数功能。

ND4J 在提供线性代数工具的平台相关库之上提供了一个抽象层。ND4J 这个名字指的是 Java 的 N 维线性代数。许多平台(例如 Windows、macOS、Linux、iOS、Android)都包含针对特定平台进行了高度优化的线性代数库。此外,特定硬件(例如,GPU)的可用性可以导致一些功能的甚至更特定的实现。

因为性能在数据科学领域非常重要,所以 Java 开发人员能够利用这些本地库提供的功能是至关重要的。然而,如果开发人员不得不编写只适用于特定硬件或操作系统配置的应用程序,这将是一个痛苦。

这就是 ND4J 及其依赖项提供解决方案的地方。ND4J 的顶层提供了用户可以与之交互的 API。这些是 Java 开发人员和科学家都很熟悉的 Java APIs,因为它们提供了线性代数库中的典型功能。在幕后,这些 API 被映射到最佳可用的本地库。

ND4J 库为希望使用数学功能的 Java 开发人员提供了很多价值,同时也为希望他们的工作能够轻松集成到新的或现有的 Java 应用程序中的研究人员和科学家提供了很多价值,无论他们是在大型企业或云环境中运行,还是在嵌入式或移动设备上运行,或者在两者之间运行。

在我们展示如何在 JShell 中使用 ND4J 之前,我们先展示一个非常简单的应用程序,它使用 ND4J 进行基本的矩阵操作。

考虑以下示例:

import org.nd4j.linalg.api.ndarray.INDArray;
import org.nd4j.linalg.factory.Nd4j;
public class HelloNd4j {public static void main(String[] args) {INDArray a = Nd4j.zeros(3,5);System.out.println("Matrix a has 3 rows and 5 columns:\n"+a);System.out.println("++++++++++++++++++++++++++++++\n");INDArray b = Nd4j.create(new double[] {0.,1.,2.,3.,4.,5.},new int[] {2,3});INDArray c = Nd4j.create(new double[] {2.,-1.,3.}, new int[] {3,1});System.out.println("Matrix b has 2 rows and 3 columns:\n"+b);System.out.println("++++++++++++++++++++++++++++++\n");System.out.println("Vector c has 3 elements:\n"+c);System.out.println("++++++++++++++++++++++++++++++\n");INDArray d = b.mmul(c);System.out.println("matrix product of b x c  =\n"+d);System.out.println("++++++++++++++++++++++++++++++\n");}
}

ND4J 库要求其他库在类路径中可用。因为我们不想为我们到底需要什么库而烦恼,所以我们将这部分委托给一个构建工具,例如 Maven。在 pom.xm 文件中,我们声明我们需要什么,Maven 将确保所有相关的依赖项都被下载并放到类路径中。

以下 pom.xml 可用于实现这一点:

<project xmlns:="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/maven-v4_0_0.xsd"><modelVersion>4.0.0</modelVersion><packaging>jar</packaging><groupId>org.modernclient</groupId><artifactId>nd4jshell</artifactId><version>1.0.0</version><url>http://maven.apache.org</url><dependencies><dependency><groupId>org.nd4j</groupId><artifactId>nd4j-native-platform</artifactId><version>1.0.0-M1</version></dependency><dependency><groupId>org.openjfx</groupId><artifactId>javafx-controls</artifactId><version>17.0.1</version></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.0</version><configuration><release>11</release></configuration></plugin><plugin><groupId>org.codehaus.mojo</groupId><artifactId>exec-maven-plugin</artifactId><version>1.6.0</version><executions><execution><goals><goal>java</goal></goals></execution></executions><configuration><mainClass>org.modernclient.HelloNd4j</mainClass></configuration></plugin></plugins></build>
</project>

如果我们使用

mvn compile exec:java

我们看到以下输出:

Matrix a has 3 rows and 5 columns:
[[         0,         0,         0,         0,         0],[         0,         0,         0,         0,         0],[         0,         0,         0,         0,         0]]
++++++++++++++++++++++++++++++
Matrix b has 2 rows and 3 columns:
[[         0,    1.0000,    2.0000],[    3.0000,    4.0000,    5.0000]]
++++++++++++++++++++++++++++++
Vector c has 3 elements:
[2.0000,-1.0000,3.0000]
++++++++++++++++++++++++++++++
matrix product of b x c  =
[5.0000,17.0000]
++++++++++++++++++++++++++++++

在这个简单的应用程序中,我们创建了几个矩阵和一个向量,并将一个矩阵和一个向量相乘。虽然这些不是特别的计算,但它们展示了 ND4J 是如何工作的。如果您想了解更多关于 ND4J 的内容,以及与之相关的项目,我们建议您看一下 https://deeplearning4j.org/docs/latest/nd4j-overview .在下一节中,我们将解释如何在 JShell 中轻松集成基于 ND4J 的应用程序。

在 JShell 中使用 ND4J

通过键入以下命令,我们可以使用与应用程序相同的类路径启动 JShell,该应用程序托管在相同的目录中

mvn compile com.github.johnpoth:jshell-maven-plugin:1.3:run

请注意,这要求 pom.xml 文件与我们键入的命令位于同一个目录中。pom.xml 文件包含应用程序的依赖关系,基于这些依赖关系(包括可传递的依赖关系),JShell-maven-plugin 组成提供给 JShell 的类路径。用于此示例的 pom 文件如下所示:

<project xmlns:="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/maven-v4_0_0.xsd"><modelVersion>4.0.0</modelVersion><packaging>jar</packaging><groupId>org.modernclient</groupId><artifactId>plotjshell</artifactId><version>1.0.0</version><url>http://maven.apache.org</url><dependencies><dependency><groupId>org.openjfx</groupId><artifactId>javafx-controls</artifactId><version>17.0.1</version></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.0</version><configuration><release>11</release></configuration></plugin><plugin><groupId>org.codehaus.mojo</groupId><artifactId>exec-maven-plugin</artifactId><version>1.6.0</version><executions><execution><goals><goal>exec</goal></goals></execution></executions><configuration><executable>java</executable><longModulepath>false</longModulepath><arguments><argument>--module-path</argument><classpath /><argument>--add-modules</argument><argument>javafx.controls</argument><argument>-classpath</argument><classpath /><argument>org.modernclient.Plot</argument></arguments></configuration></plugin></plugins></build>
</project>

因此,前面文本中显示的命令将使用应用程序中也使用的类路径启动 JShell。首先,我们导入应用程序中需要的包:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们现在从前面的应用程序中逐个输入命令。JShell 会在输入一行后立即给出输出。使用 ND4J API 的第一个命令将初始化 ND4J 后端,在此期间,将选择并初始化线性代数函数的最佳提供者。对此的反馈打印在第一个命令的结果之前。

在我们的例子中,第一个命令是创建一个 3 × 5 的矩阵,其中包含所有的零。输入此命令会生成以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Note

基于包括操作系统、CPU 和 GPU 在内的参数组合,Nd4j 库有许多不同的实现。其中一些实现提供了比其他实现更多(或不同)的日志输出,因此您可能会看到与之前粘贴的输出不同的输出。重要的一行是最后一行,在这里打印命令的结果。

我们现在可以继续输入命令并检查输出。例如,在进入声明 2 × 3 矩阵的第一行后,我们得到以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

输入最后一个命令(忽略 System.out.println)后,矩阵向量乘法的输出如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这与我们从申请中得到的结果相同。

我们现在可以用 JShell 做的一件好事就是在这个结果的基础上构建。例如,如果我们想要将结果向量中的所有元素与标量 3 相乘,我们输入命令 d.mul(3)。ND4J Javadoc 解释说 mul 命令会将矩阵元素乘以一个给定的数——参见 https://deeplearning4j.org/api/latest/org/nd4j/linalg/api/ndarray/INDArray.html#mul-java.lang.Number-

我们不必重新运行现有代码或重新编译应用程序。我们只需输入命令,结果就会立即显示出来:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最后,我们将展示一些使用 JShell 可以完成的操作。我们展示的例子没有科学意义,但是它们应该说明了 JShell 工具的灵活性,与典型的开发周期相比,典型的开发周期包括在 IDE 中修改源代码、重新编译和从头开始运行。

在执行了上一个示例中的命令后,我们想要创建一个新函数。在这个新函数中,矩阵(或向量)的所有元素都乘以一个数,然后从结果中减去另一个数。

我们将命名为 someOperation 的函数在 Java 语法中定义如下:

INDArray someOperation(INDArray src, int m, int s) {return src.mul(m).add(-s);
}

在 JShell 中定义函数与在 Java 应用程序中定义函数非常相似。我们只需输入函数定义。JShell 将确认所创建的函数,从那时起,我们可以在所有操作中使用该函数:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

例如,我们现在可以在之前创建的 b 矩阵上使用这个新函数。我们将 b 的所有元素乘以 2,然后每个元素减去 1。为了清楚起见,我们首先打印 b 的当前值,这很容易通过简单地输入 b 来完成;在 JShell 提示符下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

请注意结果前面的“$31”。当没有使用结果变量时,JShell 会自动自己创建一个变量,并将结果赋给这个变量。这些变量以后可以再次使用,类似于其他变量的使用方式。

在 JShell 中使用 JavaFX

由于 JShell 是构建在 JVM 之上的,所以任何运行在 JVM 上的库、框架或应用程序都可以使用 JShell 运行。使用 REPL 来创建 JavaFX 应用程序听起来有些矫枉过正,而且在许多情况下确实不建议这样做。然而,JavaFX 中的 Java 可视化技术允许快速可视化数据,这在开发科学应用程序时非常有用。

在展示这种快速可视化的例子之前,我们先解释如何在 JShell 中执行 JavaFX 应用程序。但是首先,我们将展示启动独立 JavaFX 应用程序的另一种方法。

启动独立 JavaFX 代码

通常,JavaFX 应用程序会扩展 javafx.application.Application。其 start 方法由 javafx 运行时调用。JavaFX 启动器管理 JavaFX 运行时的引导。

然而,我们也可以直接启动 JavaFX 运行时,并在单个方法中运行 JavaFX 应用程序。以下代码片段显示了如何做到这一点:

package org.modernclient;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class StandAlone {public static void showHello() {Platform.startup(() -> {});Platform.setImplicitExit(false);Platform.runLater( () -> {Label label = new Label ("Hello, standalone JavaFX");Button button = new Button ("Click me");button.setOnAction(e -> {label.setText("Clicked");});button.setTranslateY(50);StackPane box = new StackPane();box.getChildren().addAll(label, button);Scene s = new Scene(box, 200, 200);Stage stage = new Stage();stage.setTitle("StandAlone Hello");stage.setScene(s);stage.show();});}public static void main(String[] args) {showHello();}
}

在这个应用程序中,main 方法调用静态方法 showHello(),该方法通过调用

Platform.startup(() -> {})

该方法将启动 JavaFX 运行时,并在成功完成启动后调用提供的 Runnable。在我们的例子中,我们不会立即调用 Runnable 因此,我们传递一个空的 Runnable。一旦该方法返回,JavaFX 应用程序线程被创建,我们可以使用 Platform.runLater()语句来创建或修改 SceneGraph,类似于我们应该对 JavaFX 应用程序执行的操作,其中运行时由 JavaFX 启动器启动。

我们首先用 Maven 编译这个类,使用

mvn compile

如果您喜欢使用命令行编译,可以通过以下命令轻松完成:

javac -p /opt/javafx-sdk-17/lib --add-modules javafx.controls src/main/java/org/modernclient/StandAlone.java
where /opt/javafx-sdk-17 should be replaced with the location where you downloaded the JavaFX 17 SDK.

我们用它来运行

mvn exec:exec

或者,如果您使用命令行编译并将类编译到与源代码相同的目录中

java -p /opt/javafx-sdk-17/lib --add-modules javafx.controls -cp src/main/java/ org.modernclient.StandAlone

结果如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

为了让 Maven 正确启动这个应用程序,我们必须向 pom.xml 提供模块路径和所需的模块(javafx.controls)。

<plugin><groupId>org.codehaus.mojo</groupId><artifactId>exec-maven-plugin</artifactId><version>1.6.0</version><executions><execution><goals><goal>exec</goal></goals></execution></executions><configuration><executable>java</executable><longModulepath>false</longModulepath><arguments><argument>--module-path</argument><classpath /><argument>--add-modules</argument><argument>javafx.controls</argument><argument>-classpath</argument><classpath /><argument>org.modernclient.StandAlone</argument></arguments></configuration></plugin>

在这个代码片段中,我们配置了 exec 任务来调用 Java 命令,但是我们没有使用 Maven 本来会使用的默认启动器。相反,我们手动告诉 Java,它应该从类路径(包括依赖 javafx-controls)中获取模块路径,并添加 javafx.controls 模块。

运行这个应用程序也可以在命令行上完成。例如,如果 JavaFX SDK 安装在/opt/javafx-sdk-17 中,则以下内容有效:

java -p /opt/javafx-sdk-17/lib/ --add-modules javafx.controls -cp target/classes org.modernclient.StandAlone
JShell 中的 JavaFX 应用程序

我们现在可以在 JShell 中运行代码了。正如我们之前提到的,JShell 使用 JVM 来执行。我们可以简单地在 JShell 中输入相同的命令,输出会立即告诉我们发生了什么。

如果我们假设 JavaFX SDK 安装在/opt/javafx-sdk-11.0.12 中,以下命令将启动 JShell,正确设置模块路径,并添加 javafx.controls 模块:

jshell --module-path /opt/javafx-sdk-11.0.12/lib/ --add-modules javafx.controls

我们现在可以输入最终构成 JavaFX 应用程序的命令,从导入开始:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

注意,我们没有添加包声明。这涉及到了普通 Java 应用程序和 JShell 代码之间的一点区别。创建包的目的是公开功能并在其他组件和其他库中使用这些功能。JShell 的概念是提供一个独立的、交互式的、自包含的环境;因此,公开包没有意义。

既然已经添加了导入,我们就可以创建 showHello()方法了。我们将首先以一种效率较低的方式使用 JShell,但是在下一节中,我们将展示如何以一种更高效的方式来实现这一点。

现在,我们简单地复制粘贴 showHello 方法。这将产生以下输出:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

JShell 编辑器允许将语句或声明拆分成多行。解析器认为在输入第一行之后,还需要更多的内容。因此,它只会在我们完成方法声明之后处理它。它将检测到最后的右花括号。

在 JShell 的早期版本中,显示了一个警告,告诉我们 static 关键字被忽略了。在 JShell 中,所有的顶级声明都是静态的,所以 keyword 在这个上下文中没有用。这是常规应用程序和 JShell 上下文中的代码之间的另一个区别。

既然方法已经声明了,我们就可以调用它了。这可以通过在 JShell 提示符下调用 showHello()来完成:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

该语句立即返回,并将呈现与前面的屏幕截图相同的图像。

JShell 中的 JavaFX 库

虽然上一节中的示例可以工作,但是它非常冗长,需要手动键入或复制粘贴,并且不允许快速原型化。JavaFX 和 JShell 结合的真正好处来自提供简单内容的库和函数,这些内容可以从 JShell 语句中调用。通常,这些函数是在 Java 文件中创建的,经过编译后可供 JShell 使用。这与 JShell 如何使用 ND4J 库非常相似。

例如,我们创建了一个简单的函数,它创建了一个包含一些分散数据的 JavaFX 图表。

我们将该函数编写为常规的 Java 函数,如下面的代码所示:

package org.modernclient;
import javafx.application.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.chart.XYChart.Data;
import javafx.stage.Stage;
public class Plot {public static void scatter(double[] x, double[] y, String title) {Platform.startup(() -> {});Platform.setImplicitExit(false);Platform.runLater( () -> {NumberAxis xAxis = new NumberAxis();NumberAxis yAxis = new NumberAxis();ScatterChart chart = new ScatterChart(xAxis, yAxis);ObservableList<XYChart.Series> chartData =         FXCollections.observableArrayList();XYChart.Series<Number, Number> series = new XYChart.Series<>();ObservableList<Data<Number, Number>> data =         FXCollections.observableArrayList();for (int i = 0; i < x.length; i++) {Data<Number, Number> d = new Data<>(x[i],y[i]);data.add(d);}series.setData(data);chartData.setAll(series);chart.setData(chartData);Scene s = new Scene(chart, 400, 400);Stage stage = new Stage();stage.setTitle(title);stage.setScene(s);stage.show();});}public static void main(String[] args) {double[] x = new double[]{0.,1.,2.};double[] y = new double[]{0.,10.,16.};scatter(x, y, "plot");}
}

在这段代码中,我们定义了一个带有三个参数的 scatter 函数:一个包含 x 坐标的 double 数组、一个包含 y 坐标的 double 数组和一个图表标题。scatter 函数初始化 JavaFX 运行时,然后创建包含参数提供的数据的散点图。图表被添加到场景中并呈现在舞台上。

我们在这个类中添加了一个 main 函数,这样我们就可以使用常规的 Java 调用来测试这个函数。在这个主函数中,我们传递三个数据点(因此 x 数组有三个值,y 数组有三个值),并提供标题“plot”

为了获得图 14-6 中的 UI,我们将使用与之前相同的方法编译并运行应用程序:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14-6

运行应用程序的结果

Mvn compile exec:exec

我们现在将使这个函数对 JShell 可用,并从那里调用它。

我们使用之前使用的 Maven 插件启动 JShell:

mvn com.github.johnpoth:jshell-maven-plugin:1.1:run

确保在包含用于绘图代码的 pom.xml 文件的目录中调用该命令。这将把编译后的类和依赖项添加到类和模块路径中。

当 JShell 启动时,会显示以下消息:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们首先通过输入以下命令导入绘图类

Import org.modernclient.Plot;

我们还创建了两个数组,包含我们想要可视化的数据点的 x 和 y 值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

最后,为了创建如图 14-7 所示的图形,我们调用散布函数如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 14-7

由前面的代码生成的新绘图

Plot.scatter(c, d, "plot from jshell");

JShell 的一个很好的特性是有许多内置命令,允许它更有效地与编辑器一起工作。例如,/list 命令显示了按顺序执行的命令。成功应用前面的步骤后,/list 命令的结果如下所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这张截图清楚地表明,在 JShell 中调用预定义的函数比在 JShell 中手动声明所有函数要容易得多。根据经验,如果希望函数经常被修改,可以在 JShell 中声明它。但是如果您主要想评估函数并检查结果,那么在类中声明它们并在 JShell 环境中导入它们更合适。

结论

Java 和 JavaFX 的结合为科学工作提供了坚实的基础。Java 本身已经具有很高的性能和高度的可伸缩性。使用 JShell 和 ND4J ,(数据)科学家可以使用熟悉的线性代数例程,从事需要灵活操作数据的研究项目。

通常,设计过程中的交互式可视化缩短了开发周期并提高了结果的质量。在研究阶段引入 JavaFX 可视化允许实时可视化和高度交互式的科学应用。

https://ai-solutions.com/wp-content/uploads/2017/12/DTSE_DataSheet.pdf

http://www.lryc.cn/news/2417727.html

相关文章:

  • Unity-3D游戏开发套件指南(入门篇)-免费资源
  • 如何快速而准确地进行 IP 和端口信息扫描:渗透测试必备技能
  • PID超详细教程——PID原理+串级PID+C代码+在线仿真调参
  • 一文彻底搞懂 TSL 流程
  • 计算机网络一篇文章就能掌握,拓扑结构图文解析,协议详解等
  • Java中indexOf()的用法
  • 一文搞懂AOP 通俗易懂
  • NTFS(微软专用文件系统)
  • vault-服务器密码/证书管理工具
  • Content-type的几种常见类型及php://input的使用
  • STM32之RCC(1)
  • ASP是什么?
  • 爬虫解析——Xpath的安装及使用(五)
  • OpenCore 黑苹果安装教程
  • sonar小白式入门
  • 深度学习简介与MLP多层感知机
  • Linux命令200例:find用来查找文件和目录,不可错过的15个例子
  • AcWing 算法基础课笔记 1.基础算法
  • thmeleaf模板引擎使用总结
  • 这篇文章带你认识一款优秀国产云原生数据库 ,它就是《阿里 PolarDB》数据库
  • MySQL登录时出现Access denied for user ‘root‘@‘localhost‘ (using password: YES)无法打开的解决方法
  • HINT的30个用法
  • ARP协议详解:了解数据包转发与映射机制背后的原理
  • Source Insight 4.0使用和解决问题
  • 神经网络模型训练中的相关概念:Epoch,Batch,Batch size,Iteration
  • Yandex企业邮箱注册
  • nsfw什么颜色_“ NSFW”是什么意思,以及如何使用它?
  • 公开密钥加密算法RSA的理论概述
  • Java面试题及答案整理汇总
  • Rsync教程--linux服务器文件实时同步