ML sur Scala avec le sourire, pour ceux qui n'ont pas peur de l'expérimentation



Bonjour à tous! Aujourd'hui, nous allons parler de la mise en œuvre de l'apprentissage automatique sur Scala. Je vais commencer par expliquer comment nous sommes arrivés à une telle vie. Ainsi, notre équipe a longtemps utilisé toutes les fonctionnalités du machine learning en Python. C'est pratique, il y a beaucoup de bibliothèques utiles pour la préparation des données, une bonne infrastructure pour le développement, je veux dire Jupyter Notebook. Tout irait bien, mais face au problème de la parallélisation des calculs en production, et a décidé d'utiliser Scala dans la prod. Pourquoi pas, nous pensions, il y a des tonnes de bibliothèques là-bas, même Apache Spark est écrit en Scala! Dans le même temps, nous développons aujourd'hui des modèles en Python, puis répétons la formation en Scala pour une sérialisation plus poussée et une utilisation en production. Mais, comme on dit, le diable est dans les détails.

Immédiatement, je tiens à préciser, cher lecteur, que cet article n'a pas été écrit pour saper la réputation de Python en matière d'apprentissage automatique. Non, l'objectif principal est d'ouvrir la porte au monde de l'apprentissage automatique sur Scala, de donner un bref aperçu de l'approche alternative qui découle de notre expérience et de vous dire quelles difficultés nous avons rencontrées.

Dans la pratique, il s'est avéré que ce n'était pas si joyeux: il n'y a pas beaucoup de bibliothèques qui implémentent des algorithmes d'apprentissage automatique classiques, mais celles qui sont souvent des projets OpenSource sans le soutien de grands fournisseurs. Oui, bien sûr, il y a Spark MLib, mais il est fortement lié à l'écosystème Apache Hadoop, et je ne voulais vraiment pas le faire glisser dans l'architecture de microservice.

Ce qu'il fallait, c'était une solution qui sauverait le monde et ramènerait un sommeil réparateur, et elle a été trouvée!

De quoi avez-vous besoin?


Lorsque nous avons choisi un outil d'apprentissage automatique, nous sommes partis des critères suivants:

  • cela devrait ĂŞtre simple;
  • malgrĂ© sa simplicitĂ©, personne n'a annulĂ© une large fonctionnalitĂ©;
  • Je voulais vraiment pouvoir dĂ©velopper des modèles dans l'interprĂ©teur Web, et non via la console ou les assemblages et compilations constants;
  • la disponibilitĂ© de la documentation joue un rĂ´le important;
  • idĂ©alement, il y aurait un support au moins pour rĂ©pondre aux problèmes de github.

Qu'avons-nous vu?


  • Apache Spark MLib : ne nous convenait pas. Comme mentionnĂ© ci-dessus, cet ensemble de bibliothèques est fortement liĂ© Ă  la pile Apache Hadoop et Ă  Spark Core lui-mĂŞme, qui pèse trop pour construire des microservices basĂ©s sur lui.
  • Apache PredictionIO : un projet intĂ©ressant, de nombreux contributeurs, il y a de la documentation avec des exemples. En fait, il s'agit d'un serveur REST sur lequel les modèles tournent. Il existe des modèles prĂŞts Ă  l'emploi, par exemple, la classification des textes, dont le lancement est dĂ©crit dans la documentation. La documentation dĂ©crit comment ajouter et former vos modèles. Nous ne nous sommes pas adaptĂ©s, car Spark est utilisĂ© sous le capot, et cela relève davantage du domaine d'une solution monolithique, plutĂ´t que d'une architecture de microservice.
  • Apache MXNet : un cadre intĂ©ressant pour travailler avec des rĂ©seaux de neurones, il existe un support pour Scala et Python - c'est pratique, vous pouvez former un rĂ©seau de neurones en Python, puis charger le rĂ©sultat enregistrĂ© de Scala lors de la crĂ©ation d'une solution de production. Nous l'utilisons dans des solutions de production, il y a un article sĂ©parĂ© Ă  ce sujet ici .
  • Smile : très similaire au package scikit-learn pour Python. Il existe de nombreuses implĂ©mentations d'algorithmes classiques d'apprentissage automatique, une bonne documentation avec des exemples, un support sur github, un visualiseur intĂ©grĂ© (propulsĂ© par Swing), vous pouvez utiliser Jupyter Notebook pour dĂ©velopper des modèles. C'est exactement ce dont vous avez besoin!

Préparation de l'environnement


Nous avons donc choisi Smile. Je vais vous expliquer comment l'exécuter dans le bloc-notes Jupyter en utilisant l'algorithme de clustering k-means comme exemple. La première chose que nous devons faire est d'installer Jupyter Notebook avec le support Scala. Cela peut être fait via pip ou utiliser une image Docker déjà assemblée et configurée. Je suis pour une deuxième option plus simple.

Pour me faire des amis de Jupyter avec Scala, je voulais utiliser BeakerX, qui fait partie de l'image Docker, disponible dans le dépôt officiel de BeakerX. Cette image est recommandée dans la documentation Smile, et vous pouvez l'exécuter comme ceci:

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

Mais ici, le premier problème attendait: au moment de la rédaction de l'article, BeakerX 1.0.0 était installé dans l'image beakerx / beakerx, et la version 1.4.1 était déjà disponible dans le github officiel du projet (plus précisément, la dernière version 1.3.0, mais l'assistant contient 1.4.1, et ça marche :-)).

Il est clair que je veux travailler avec la dernière version, j'ai donc créé ma propre image basée sur BeakerX 1.4.1. Je ne vous ennuierai pas avec le contenu du Dockerfile, voici un lien vers celui-ci.

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

Soit dit en passant, pour ceux qui utiliseront mon image, il y aura un petit bonus: dans le répertoire des exemples, il y a un exemple k-means pour une séquence aléatoire avec traçage (ce n'est pas une tâche complètement triviale pour les cahiers Scala).

Télécharger Smile in Jupyter Notebook


Excellent environnement préparé! Nous créons un nouveau cahier Scala dans un dossier de notre répertoire, puis nous devons télécharger les bibliothèques de Maven pour que Smile fonctionne.

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

Après avoir exécuté le code, une liste des fichiers jar téléchargés apparaîtra dans son bloc de sortie.

Étape suivante: importation des packages nécessaires au fonctionnement de l'exemple.

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

Préparation des données pour le clustering


Nous allons maintenant résoudre le problème suivant: générer une image composée de zones de trois couleurs primaires - rouge, vert et bleu (R, G, B). L'une des couleurs de l'image prévaudra. Nous regroupons les pixels de l'image, prenons le cluster dans lequel il y aura le plus de pixels, changeons leur couleur en gris et construisons une nouvelle image à partir de tous les pixels. Résultat attendu: la zone de couleur prédominante deviendra grise, le reste de la zone ne changera pas de couleur.

 //    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 

À la suite de l'exécution de ce code, l'image suivante s'affiche:



Étape suivante: convertir l'image en un ensemble de pixels. Par pixel, nous entendons une entité avec les propriétés suivantes:

  • coordonnĂ©e latĂ©rale large (x);
  • coordonnĂ©e latĂ©rale Ă©troite (y);
  • valeur de couleur;
  • valeur facultative du numĂ©ro de classe / cluster (avant la fin du clustering, il sera vide).

En tant qu'entité, il est pratique d'utiliser la case class :

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

Ici, pour les valeurs de couleur, le tableau rgbArray de trois valeurs de rouge, vert et bleu est utilisé (par exemple, pour le Array(255.0, 0, 0) couleur rouge 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) 

Ceci termine la préparation des données.

Regroupement des couleurs des pixels


Nous avons donc une collection de pixels de trois couleurs primaires, nous allons donc regrouper les pixels en trois classes.

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

La documentation recommande de définir le paramètre runs dans une plage de 10 à 20.

Lorsque ce code est exécuté, un objet de type KMeans sera créé. Le bloc de sortie contiendra des informations sur les résultats du clustering:

 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%) 

Un cluster contient plus de pixels que les autres. Maintenant, nous devons marquer notre collection de pixels avec des classes de 0 Ă  2.

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

Repeindre l'image


La seule chose qui reste est de sélectionner le cluster avec le plus grand nombre de pixels et de repeindre tous les pixels inclus dans ce cluster en gris (changer la valeur du tableau 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) 

Il n'y a rien de compliqué, juste grouper par numéro de cluster (c'est notre Option:[Int] ), compter le nombre d'éléments dans chaque groupe et extraire le cluster avec le nombre maximum d'éléments. Ensuite, changez la couleur en gris uniquement pour les pixels qui appartiennent au cluster trouvé.

Créez une nouvelle image et enregistrez les résultats.


Rassembler une nouvelle image de la collection de pixels:

 //       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 

C'est ce que nous avons finalement fait.



Nous enregistrons les deux images.

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

Conclusion


L'apprentissage automatique sur Scala existe. Pour implémenter les algorithmes de base, il n'est pas nécessaire de faire glisser une énorme bibliothèque. L'exemple ci-dessus montre que pendant le développement, vous ne pouvez pas abandonner les moyens habituels, le même bloc-notes Jupyter peut facilement devenir ami avec Scala.

Bien sûr, pour un aperçu complet de toutes les fonctionnalités de Smile, un article ne suffit pas, et cela n'était pas inclus dans les plans. La tâche principale - ouvrir la porte au monde de l'apprentissage automatique sur Scala - est, je pense, terminée. À vous de décider si vous souhaitez utiliser ces outils, et plus encore, les faire glisser en production ou non!

Les références


Source: https://habr.com/ru/post/fr452914/


All Articles