面向微笑不怕实验的人,Scala上的ML面带微笑



大家好! 今天我们将讨论在Scala上机器学习的实现。 我将从解释我们如何过这种生活开始。 因此,我们的团队长期使用Python中机器学习的所有功能。 这很方便,有很多有用的库可用于数据准备,良好的开发基础设施,我的意思是Jupyter Notebook。 一切都很好,但是面临着在生产中并行化计算的问题,因此决定在产品中使用Scala。 我们以为为什么不存在大量的库,即使Apache Spark是用Scala编写的! 同时,今天,我们使用Python开发模型,然后重复在Scala中进行培训,以进行进一步的序列化并在生产中使用。 但是,正如他们所说,魔鬼在细节中。

亲爱的读者,我想立即澄清一下,这篇文章并不是为了削弱Python在机器学习方面的声誉。 不,主要目的是打开Scala上的机器学习世界的大门,简要概述根据我们的经验得出的替代方法,并告诉您我们遇到了什么困难。

在实践中,事实证明这并不那么快乐:实现经典机器学习算法的库并不多,而那些通常是没有大型供应商支持的OpenSource项目。 是的,当然有Spark MLib,但是它与Apache Hadoop生态系统紧密相关,我真的不想将其拖入微服务架构中。

所需要的是一种可以拯救世界并带回安宁的睡眠的解决方案,结果被发现!

你需要什么


当我们选择机器学习工具时,我们遵循以下标准:

  • 它应该很简单;
  • 尽管简单,但没有人取消广泛的功能;
  • 我真的希望能够在Web解释器中开发模型,而不是通过控制台或常量汇编和编译来开发模型。
  • 文件的可用性起着重要作用;
  • 理想情况下,至少应该支持github问题。

我们看到了什么?


  • Apache Spark MLib :不适合我们。 如上所述,这组库与Apache Hadoop堆栈和Spark Core本身紧密相关,后者的权重太大,无法基于该库构建微服务。
  • Apache PredictionIO :一个有趣的项目,许多贡献者,都有带示例的文档。 实际上,这是一个正在旋转模型的REST服务器。 有现成的模型,例如文本分类,其启动过程在文档中进行了描述。 该文档描述了如何添加和训练模型。 我们不适合,因为Spark是在引擎盖下使用的,而这更多的是单片解决方案而不是微服务架构。
  • Apache MXNet :一个有趣的使用神经网络的框架,支持Scala和Python-这很方便,您可以使用Python训练神经网络,然后在创建生产解决方案时从Scala加载保存的结果。 我们在生产解决方案中使用它,这里有单独的文章。
  • Smile :非常类似于Python的scikit-learn软件包。 经典机器学习算法的实现方式很多,有示例的优质文档,github上的支持,集成的可视化工具(由Swing支持),您可以使用Jupyter Notebook开发模型。 这就是您所需要的!

环境准备


因此,我们选择了Smile。 我将以k-means聚类算法为例,告诉您如何在Jupyter Notebook中运行它。 我们需要做的第一件事是安装具有Scala支持的Jupyter Notebook。 这可以通过pip完成,也可以使用已组装和配置的Docker映像。 我是一个更简单的第二选择。

为了让Jupyter与Scala成为朋友,我想使用BeakerX,它是Docker映像的一部分,可在官方BeakerX存储库中获得。 建议在Smile文档中使用该图像,您可以像这样运行它:

#   BeakerX docker run -p 8888:8888 beakerx/beakerx 

但是这里遇到的第一个麻烦是在等待:在撰写本文时,Beakerx / beakerx映像内已安装了BeakerX 1.0.0,并且该项目的官方github中已经有1.4.1版本(更确切地说,最新版本1.3.0,但该向导包含1.4.1,它的工作原理:-))。

很明显,我想使用最新版本,因此我根据BeakerX 1.4.1整理了自己的图像。 我不会对Dockerfile的内容感到厌烦,这里是它的链接

 #         mkdir -p /tmp/my_code docker run -it \ -p 8888:8888 \ -v /tmp/my_code:/workspace/my_code \ entony/jupyter-scala:1.4.1 

顺便说一下,对于那些将使用我的图像的人来说,这将是一笔不小的收获:在examples目录中,有一个示例k均值用于绘制随机序列(对于Scala笔记本而言,这并不是一件完全琐碎的任务)。

下载Jupyter Notebook中的Smile


良好的准备环境! 我们在目录的文件夹中创建一个新的Scala笔记本,然后需要从Maven下载这些库才能使Smile正常工作。

 %%classpath add mvn com.github.haifengl smile-scala_2.12 1.5.2 

执行代码后,已下载的jar文件列表将出现在其输出块中。

下一步:导入必要的程序包以使示例正常工作。

 import java.awt.image.BufferedImage import java.awt.Color import javax.imageio.ImageIO import java.io.File import smile.clustering._ 

准备数据进行聚类


现在我们将解决以下问题:生成由三个原色区域组成的图像-红色,绿色和蓝色(R,G,B)。 图片中的一种颜色为准。 我们对图像的像素进行聚类,对其中像素最多的聚类进行聚类,将其颜色更改为灰色,然后根据所有像素构建一个新图像。 预期结果:主要颜色的区域将变为灰色,其余区域将不会改变其颜色。

 //    640  360 val width = 640 val hight = 360 //      val testImage = new BufferedImage(width, hight, BufferedImage.TYPE_INT_RGB) //   .    . for { x <- (0 until width) y <- (0 until hight) color = if (y <= hight / 3 && (x <= width / 3 || x > width / 3 * 2)) Color.RED else if (y > hight / 3 * 2 && (x <= width / 3 || x > width / 3 * 2)) Color.GREEN else Color.BLUE } testImage.setRGB(x, y, color.getRGB) //    testImage 

执行此代码后,将显示以下图片:



下一步:将图片转换为一组像素。 像素表示具有以下属性的实体:

  • 宽边坐标(x);
  • 窄边坐标(y);
  • 颜色值
  • 类/集群号的可选值(在集群完成之前,它将为空)。

作为一个实体,使用case class很方便:

 case class Pixel(x: Int, y: Int, rgbArray: Array[Double], clusterNumber: Option[Int] = None) 

在此,对于颜色值,使用由红色,绿色和蓝色三个值组成的rgbArray数组(例如,对于红色Array(255.0, 0, 0) )。

 //      (Pixel) val pixels = for { x <- (0 until testImage.getWidth).toArray y <- (0 until testImage.getHeight) color = new Color(testImage.getRGB(x, y)) } yield Pixel(x, y, Array(color.getRed.toDouble, color.getGreen.toDouble, color.getBlue.toDouble)) //   10   pixels.take(10) 

这样就完成了数据准备。

像素颜色聚类


因此,我们有三种原色的像素集合,因此我们将像素分为三个类别。

 //   val countColors = 3 //   val clusters = kmeans(pixels.map(_.rgbArray), k = countColors, runs = 20) 

文档建议将runs参数设置为10到20。

执行此代码后,将创建KMeans类型的对象。 输出块将包含有关聚类结果的信息:

 K-Means distortion: 0.00000 Clusters of 230400 data points of dimension 3: 0 50813 (22.1%) 1 51667 (22.4%) 2 127920 (55.5%) 

一个群集确实包含比其余群集更多的像素。 现在我们需要用0到2的类标记我们的像素集合。

 //    val clusteredPixels = (pixels zip clusters.getClusterLabel()).map {case (pixel, cluster) => pixel.copy(clusterNumber = Some(cluster))} //  10   clusteredPixels.take(10) 

重画图像


剩下的唯一事情是选择像素数最多的群集,并将此群集中包含的所有像素重新绘制为灰色(更改rgbArray数组的值)。

 //   val grayColor = Array(127.0, 127.0, 127.0) //       val blueClusterNumber = clusteredPixels.groupBy(pixel => pixel.clusterNumber) .map {case (clusterNumber, pixels) => (clusterNumber, pixels.size) } .maxBy(_._2)._1 //       val modifiedPixels = clusteredPixels.map { case p: Pixel if p.clusterNumber == blueClusterNumber => p.copy(rgbArray = grayColor) case p: Pixel => p } //  10      modifiedPixels.take(10) 

没什么复杂的,只需按簇号分组(这是我们的Option:[Int] ),计算每个组中的元素数,然后取出具有最大元素数的簇。 接下来,仅将属于找到的群集的那些像素的颜色更改为灰色。

创建一个新图像并保存结果。


从像素集合中收集新图像:

 //       val modifiedImage = new BufferedImage(width, hight, BufferedImage.TYPE_INT_RGB) //     modifiedPixels.foreach { case Pixel(x, y, rgbArray, _) => val r = rgbArray(0).toInt val g = rgbArray(1).toInt val b = rgbArray(2).toInt modifiedImage.setRGB(x, y, new Color(r, g, b).getRGB) } //    modifiedImage 

最终,我们做到了。



我们保存两个图像。

 ImageIO.write(testImage, "png", new File("testImage.png")) ImageIO.write(modifiedImage, "png", new File("modifiedImage.png")) 

结论


存在于Scala上的机器学习。 要实现基本算法,无需拖动一些巨大的库。 上面的示例表明,在开发过程中您不能放弃通常的方法,同一个Jupyter Notebook可以很容易地与Scala成为朋友。

当然,要获得有关Smile的所有功能的完整概述,仅一篇文章是不够的,并且未包含在计划中。 我认为完成主要任务-打开Scala上的机器学习世界之门。 是否使用这些工具,甚至更多地将它们拖入生产环境,完全取决于您!

参考文献


Source: https://habr.com/ru/post/zh-CN452914/


All Articles