- PVSM.RU - https://www.pvsm.ru -
Всем привет! Сегодня будем говорить о реализации машинного обучения на Scala. Начну с объяснения, как мы докатились до такой жизни. Итак, наша команда долгое время использовала все возможности машинного обучения на Python. Это удобно, есть много полезных библиотек для подготовки данных, хорошая инфраструктура для разработки, я имею в виду Jupyter Notebook. Всё бы ничего, но столкнулись с проблемой распараллеливания вычислений в production, и решили использовать в проде Scala. Почему бы и нет, подумали мы, там есть куча библиотек, даже Apache Spark написан на Scala! При этом, сегодня модели мы разрабатываем на Python, а затем повторяем обучение на Scala для дальнейшей сериализации и использования в production. Но, как говорится, дьявол кроется в деталях.
Сразу хочу внести ясность, дорогой читатель, эта статья написана не с целью пошатнуть репутацию Python в вопросах машинного обучения. Нет, основная цель — приоткрыть дверь в мир машинного обучения на Scala, сделать небольшой обзор альтернативного подхода, вытекающего из нашего опыта, и рассказать, с какими трудностями мы столкнулись.
На практике оказалось не так уж всё и радостно: не так много библиотек, реализующих классические алгоритмы машинного обучения, а те, что есть — это, зачастую, OpenSource-проекты без поддержки крупных вендоров. Да, безусловно, есть Spark MLib, но он сильно привязан к экосистеме Apache Hadoop, да и тащить его в микросервисную архитектуру уж очень не хотелось.
Нужно было решение, которое спасёт мир и вернёт спокойный сон, и оно было найдено!
Когда мы выбирали инструмент для машинного обучения, то исходили из таких критериев:
Итак, мы выбрали Smile. Расскажу, как запустить его в Jupyter Notebook на примере алгоритма кластеризации k-means. Первое, что нам нужно сделать — установить Jupyter Notebook с поддержкой Scala. Это можно сделать через 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, вот ссылка [6] на него.
# Запускаем образ и монтируем в него рабочую директорию
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-means для случайной последовательности с построением графика (это не совсем тривиальная задача для Scala notebooks).
Отлично, окружение подготовили! Создаём в папке в нашей директории новый Scala notebooks, далее необходимо выкачать из 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
В результате выполнения этого кода выводится вот такая картинка:
Следующий шаг: преобразуем картинку в набор пикселей. Под пикселем будем понимать сущность с такими свойствами:
В качестве сущности удобно использовать 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(testImageWidth, testImageHight, 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 — считаю выполненной. Пользоваться ли этими инструментами, и, уж тем более, тащить их в production или нет, решать вам!
Автор: entony
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/scala/318428
Ссылки в тексте:
[1] Apache Spark MLib: https://spark.apache.org/mllib/
[2] Apache PredictionIO: http://predictionio.apache.org/index.html
[3] Apache MXNet: https://mxnet.incubator.apache.org
[4] тут: https://habr.com/ru/company/mailru/blog/439226/
[5] Smile: https://haifengl.github.io/smile/
[6] ссылка: https://github.com/AntonYurchenko/docker/blob/master/jupyter-scala/Dockerfile
[7] https://github.com/AntonYurchenko/habr/tree/master/scala-smile-clustering: https://github.com/AntonYurchenko/habr/tree/master/scala-smile-clustering
[8] https://mxnet.incubator.apache.org: https://mxnet.incubator.apache.org/
[9] https://github.com/twosigma/beakerx: https://github.com/twosigma/beakerx
[10] Источник: https://habr.com/ru/post/452914/?utm_campaign=452914
Нажмите здесь для печати.