ML em Scala com um sorriso, para aqueles que não têm medo de experimentação



Olá pessoal! Hoje falaremos sobre a implementação do aprendizado de máquina no Scala. Vou começar explicando como chegamos a essa vida. Portanto, nossa equipe utilizou por muito tempo todos os recursos do aprendizado de máquina no Python. É conveniente, existem muitas bibliotecas úteis para preparação de dados, uma boa infra-estrutura para o desenvolvimento, quero dizer, o Jupyter Notebook. Tudo ficaria bem, mas confrontado com o problema de paralelizar cálculos na produção, e decidiu usar Scala no produto. Por que não, pensamos, existem toneladas de bibliotecas por aí, até o Apache Spark está escrito em Scala! Ao mesmo tempo, hoje desenvolvemos modelos em Python e repetimos o treinamento no Scala para posterior serialização e uso na produção. Mas, como se costuma dizer, o diabo está nos detalhes.

Imediatamente, quero esclarecer, caro leitor, que este artigo não foi escrito para minar a reputação do Python de aprendizado de máquina. Não, o objetivo principal é abrir as portas para o mundo do aprendizado de máquina no Scala, fornecer uma breve visão geral da abordagem alternativa que se segue da nossa experiência e explicar as dificuldades que encontramos.

Na prática, verificou-se que não era tão divertido: não existem muitas bibliotecas que implementam os algoritmos clássicos de aprendizado de máquina e aquelas que são geralmente projetos de código aberto sem o apoio de grandes fornecedores. Sim, é claro, existe o Spark MLib, mas está fortemente ligado ao ecossistema Apache Hadoop, e eu realmente não queria arrastá-lo para a arquitetura de microsserviço.

O que era necessário era uma solução que salvasse o mundo e traria de volta um sono reparador, e foi encontrada!

Do que você precisa?


Quando escolhemos uma ferramenta para aprendizado de máquina, partimos dos seguintes critérios:

  • deveria ser simples;
  • apesar de sua simplicidade, ninguém cancelou uma ampla funcionalidade;
  • Eu realmente queria ser capaz de desenvolver modelos no intérprete da Web, e não através do console ou montagens e compilações constantes;
  • a disponibilidade da documentação desempenha um papel importante;
  • idealmente, haveria suporte pelo menos para responder a problemas do github.

O que vimos?


  • Apache Spark MLib : não nos convinha . Como mencionado acima, esse conjunto de bibliotecas está fortemente vinculado à pilha do Apache Hadoop e ao próprio Spark Core, que pesa demais para criar microsserviços com base nela.
  • Apache PredictionIO : um projeto interessante, muitos colaboradores, há documentação com exemplos. De fato, este é um servidor REST no qual os modelos estão girando. Existem modelos prontos, por exemplo, classificação de texto, cujo lançamento está descrito na documentação. A documentação descreve como você pode adicionar e treinar seus modelos. Não nos encaixamos, pois o Spark é usado sob o capô, e isso é mais da área de uma solução monolítica do que de uma arquitetura de microsserviço.
  • Apache MXNet : uma estrutura interessante para trabalhar com redes neurais, há suporte para Scala e Python - isso é conveniente, você pode treinar uma rede neural em Python e carregar o resultado salvo do Scala ao criar uma solução de produção. Nós o usamos em soluções de produção, há um artigo separado sobre isso aqui .
  • Sorriso : muito semelhante ao pacote scikit-learn para Python. Existem muitas implementações de algoritmos clássicos de aprendizado de máquina, boa documentação com exemplos, suporte no github, um visualizador integrado (desenvolvido pela Swing), você pode usar o Jupyter Notebook para desenvolver modelos. Isto é exatamente o que você precisa!

Preparação do ambiente


Então, escolhemos o Smile. Vou explicar como executá-lo no Jupyter Notebook usando o algoritmo de agrupamento k-means como exemplo. A primeira coisa que precisamos fazer é instalar o Jupyter Notebook com suporte para Scala. Isso pode ser feito via pip ou use uma imagem do Docker já montada e configurada. Sou a favor de uma segunda opção mais simples.

Para fazer Jupyter fazer amizade com Scala, eu queria usar o BeakerX, que faz parte da imagem do Docker, disponível no repositório oficial do BeakerX. Esta imagem é recomendada na documentação do Smile e você pode executá-la assim:

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

Mas aqui estava o primeiro problema: no momento da redação do artigo, o BeakerX 1.0.0 estava instalado na imagem beakerx / beakerx e a versão 1.4.1 já estava disponível no github oficial do projeto (mais precisamente, na versão mais recente 1.3.0, mas o assistente contém 1.4.1, e funciona :-)).

Está claro que eu quero trabalhar com a versão mais recente, então montei minha própria imagem com base no BeakerX 1.4.1. Não vou aborrecê-lo com o conteúdo do Dockerfile, aqui está um link para ele.

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

A propósito, para aqueles que usarão minha imagem, haverá um pequeno bônus: no diretório de exemplos, existe um exemplo de k-means para uma sequência aleatória com plotagem (essa não é uma tarefa completamente trivial para os notebooks Scala).

Faça o download do Smile in Jupyter Notebook


Excelente ambiente preparado! Criamos um novo bloco de notas Scala em uma pasta em nosso diretório e precisamos fazer o download das bibliotecas do Maven para que o Smile funcione.

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

Após executar o código, uma lista de arquivos jar baixados aparecerá em seu bloco de saída.

Próxima etapa: importando os pacotes necessários para o exemplo funcionar.

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

Preparando Dados para Armazenamento em Cluster


Agora vamos resolver o seguinte problema: gerar uma imagem composta por zonas de três cores primárias - vermelho, verde e azul (R, G, B). Uma das cores na imagem prevalecerá. Agrupamos os pixels da imagem, pegamos o cluster no qual haverá mais pixels, alteramos sua cor para cinza e construímos uma nova imagem a partir de todos os pixels. Resultado esperado: a zona da cor predominante ficará cinza, o restante da zona não mudará de cor.

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

Como resultado da execução desse código, a seguinte imagem é exibida:



Próxima etapa: converta a imagem em um conjunto de pixels. Por pixel, entendemos uma entidade com as seguintes propriedades:

  • coordenada lateral larga (x);
  • coordenada lateral estreita (y);
  • valor da cor;
  • valor opcional do número de classe / cluster (antes que o cluster seja concluído, ele estará vazio).

Como entidade, é conveniente usar a case class :

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

Aqui, para os valores de cor, é rgbArray matriz rgbArray de três valores de vermelho, verde e azul (por exemplo, para a Array(255.0, 0, 0) cor vermelha 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) 

Isso completa a preparação dos dados.

Cluster de cores de pixel


Portanto, temos uma coleção de pixels de três cores primárias, portanto, agruparemos os pixels em três classes.

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

A documentação recomenda definir o parâmetro de runs no intervalo de 10 a 20.

Quando esse código é executado, um objeto do tipo KMeans será criado. O bloco de saída conterá informações sobre os resultados do armazenamento em cluster:

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

Um cluster contém mais pixels que o restante. Agora precisamos marcar nossa coleção de pixels com classes de 0 a 2.

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

Repintar imagem


A única coisa que resta é selecionar o cluster com o maior número de pixels e repintar todos os pixels incluídos neste cluster para cinza (altere o valor da matriz 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) 

Não há nada complicado, basta agrupar por número de cluster (esta é a nossa Option:[Int] ), contar o número de elementos em cada grupo e retirar o cluster com o número máximo de elementos. Em seguida, altere a cor para cinza apenas para os pixels que pertencem ao cluster encontrado.

Crie uma nova imagem e salve os resultados.


Reunindo uma nova imagem da coleção 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 

Foi o que, no final, fizemos.



Nós salvamos as duas imagens.

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

Conclusão


O aprendizado de máquina no Scala existe. Para implementar os algoritmos básicos, não é necessário arrastar uma grande biblioteca. O exemplo acima mostra que, durante o desenvolvimento, você não pode desistir dos meios usuais, o mesmo Notebook Jupyter pode ser facilmente amigo de Scala.

Obviamente, para uma visão geral completa de todos os recursos do Smile, um artigo não é suficiente e isso não foi incluído nos planos. A principal tarefa - abrir a porta para o mundo do aprendizado de máquina no Scala - acho que está concluída. A decisão de usar essas ferramentas e, mais ainda, arrastá-las para a produção, depende de você!

Referências


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


All Articles