Scala + MXNet = Microservicio con neurona en prod


En Internet hay una gran cantidad de manuales y ejemplos, sobre la base de los cuales ustedes, queridos lectores, podrán "sin mucha dificultad" y con costos de tiempo "mínimos" escribir código que pueda distinguir a los gatos de los perros en una foto. ¿Y por qué perder tiempo en este artículo?

El principal inconveniente, en mi opinión, de todos estos ejemplos son las posibilidades limitadas. Tomó un ejemplo, incluso con la red neuronal básica que ofrece el autor, lo lanzó, tal vez incluso funcionó, y ¿qué sigue? ¿Cómo hacer que este código simple comience a funcionar en un servidor de producción? ¿Cómo actualizarlo y mantenerlo? Aquí es donde comienza la diversión. No pude encontrar una descripción completa del proceso desde el momento "Bueno, el ingeniero de ML entrenó la red neuronal" hasta que "finalmente la implementamos en producción". Y decidí cerrar esta brecha.

No hablaré sobre cómo enseñarle a la red neuronal nuevas cosas divertidas que te complacerán y te ayudarán a ganar un montón de billetes crujientes. Este es un gran tema para un artículo separado. Como ejemplo, usaré una red neuronal que se puede descargar libremente. La tarea principal que me propuse es dar una descripción completa del proceso de introducción de una red neuronal en funcionamiento.

Inmediatamente respondo la pregunta "¿Por qué no en Python?": Utilizamos Scala para soluciones de producción debido a la escritura más conveniente y estable de código multiproceso.

Contenido


1. Declaración del problema
2. Tecnologías utilizadas
3. Preparando un contenedor básico
4. Estructura del proyecto.
5. Carga de la red neuronal
6. Implementación de la API REST
7. Prueba
8. Ensamblar un microservicio basado en una imagen básica
9. Inicio de un microservicio en un servidor de producción con una GPU
Conclusión
Referencias

1. Declaración del problema


Supongamos que tenemos una gran base de datos de fotos con diferentes objetos, y necesitamos hacer un microservicio que reciba una imagen en una solicitud HTTP POST y responda en formato JSON. La respuesta debe contener el número de objetos encontrados y sus clases, el grado de probabilidad de que este sea exactamente el objeto de la clase declarada y las coordenadas de los rectángulos que cubren los límites de cada objeto.

2. Tecnologías utilizadas


  • Scala 2.12.7 + conjunto mínimo de bibliotecas adicionales, Sbt 1.2.6 con el complemento Sbt-pack 0.12 para construir códigos fuente.
  • MXNet 1.3.1 (la última versión estable en el momento de la escritura), compilada para Scala 2.12.
  • Servidor con tarjetas gráficas Nvidia.
  • Cuda 9.0 y Cudnn 7 instalados en el servidor.
  • Java 8 para ejecutar código compilado.
  • Docker para facilitar el montaje, la entrega y el lanzamiento del microservicio en el servidor.

3. Preparando un contenedor básico


Para nuestro microservicio, necesitará una imagen básica de Docker en la que se instalará la cantidad mínima de dependencias necesarias para ejecutar. Para el montaje, utilizaremos la imagen con Sbt instalado adicionalmente. Sí, crearemos las propias fuentes no en el entorno local, sino en el contenedor Docker. Esto facilitará la transición hacia el ensamblaje a través de CI, por ejemplo, a través de gitlab CI.

Estructura de la carpeta:

\ | ----- install | | ----- java8.sh | | ----- mxnet_2_12.sh | | ----- opencv.sh | | ----- sbt.sh | | ----- scala.sh | | ----- timeZone.sh | ----- scala-mxnet-cuda-cudnn | ----- Dockerfile.2.12-1.3.1-9-7-builder | ----- Dockerfile.2.12-1.3.1-9-7-runtime 

Dockerfile.2.12-1.3.1-9-7-runtime


Esta imagen se utilizará para el lanzamiento final del microservicio. Se basa en la imagen oficial de Nvidia con CUDA 9.0 y CUDNN 7. preinstalados. La documentación para MXNet 1.3.1 afirma que funciona con CUDA 8.0, pero, como la práctica ha demostrado, todo funciona bien con la versión 9.0, e incluso un poco más rápido.

Además, instalaremos Java 8, MXNet 1.3.1 (lo crearemos en Scala 2.12), OpenCV 3.4.3 y la utilidad Linux para configurar la zona horaria en esta imagen.

 #        Nvidia  cuda 9.0  cudnn 7 FROM nvidia/cuda:9.0-cudnn7-devel AS builder #    ENV MXNET_VERSION 1.3.1 ENV MXNET_BUILD_OPT "USE_OPENCV=1 USE_BLAS=openblas USE_CUDA=1 USE_CUDA_PATH=/usr/local/cuda USE_CUDNN=1" ENV CUDA_STUBS_DIR "/usr/local/cuda-9.0/targets/x86_64-linux/lib/stubs" ENV OPEN_CV_VERSION 3.4.3 ENV OPEN_CV_INSTALL_PREFIX /usr/local ENV JAVA_HOME /usr/lib/jvm/java-8-oracle/ ENV TIME_ZONE Europe/Moscow #     COPY install /install RUN chmod +x -R /install/* #   RUN apt-get update WORKDIR /install RUN ./timeZone.sh ${TIME_ZONE} RUN ./java8.sh RUN ./mxnet_2_12.sh ${MXNET_VERSION} "${MXNET_BUILD_OPT}" ${CUDA_STUBS_DIR} RUN ./opencv.sh ${OPEN_CV_VERSION} ${OPEN_CV_INSTALL_PREFIX} #     RUN apt-get autoclean -y && \ rm -rf /var/cache/* /install #       FROM nvidia/cuda:9.0-cudnn7-devel COPY --from=builder --chown=root:root / / 

Los scripts timeZone.sh java8.sh y opencv.sh son bastante triviales, por lo que no me detendré en ellos en detalle, se presentan a continuación.

timeZone.sh


 #!/bin/sh #         TIME_ZONE=${1} #       apt-get install -y tzdata && \ ln -sf /usr/share/zoneinfo/$TIME_ZONE /etc/localtime && \ dpkg-reconfigure -f noninteractive tzdata 

java8.sh


 #!/bin/sh #  Java 8 apt-get install -y software-properties-common && \ add-apt-repository ppa:webupd8team/java -y && \ apt-get update && \ echo "oracle-java8-installer shared/accepted-oracle-license-v1-1 select true" | debconf-set-selections && \ apt-get install -y oracle-java8-installer 

opencv.sh


 #!/bin/sh #   OpenCV     OPEN_CV_VERSION=${1} #        OPEN_CV_INSTALL_PREFIX=${2} OPEN_CV_TAR="http://github.com/opencv/opencv/archive/${OPEN_CV_VERSION}.tar.gz" #  OpenCV apt-get install -y wget build-essential cmake && \ wget -qO- ${OPEN_CV_TAR} | tar xzv -C /tmp && \ mkdir /tmp/opencv-${OPEN_CV_VERSION}/build && \ cd /tmp/opencv-${OPEN_CV_VERSION}/build && \ cmake -DBUILD_JAVA=ON -DCMAKE_INSTALL_PREFIX:PATH=${OPEN_CV_INSTALL_PREFIX} .. && \ make -j$((`nproc`+1)) && \ make install && \ rm -rf /tmp/opencv-${OPEN_CV_VERSION} 

Instalar MXNet no es tan simple. El hecho es que todos los ensamblajes de esta biblioteca para Scala se realizan sobre la base de la versión del compilador 2.11, y esto está justificado, ya que la biblioteca incluye un módulo para trabajar con Spark, que, a su vez, está escrito en Scala 2.11. Teniendo en cuenta que usamos Scala 2.12.7 en el desarrollo, las bibliotecas compiladas no son adecuadas para nosotros, y no podemos pasar a la versión 2.11. * No podemos, debido a la gran cantidad de código ya escrito en la nueva versión de Scala. Que hacer Diviértase mucho recolectando MXNet de la fuente para nuestra versión de Scala. A continuación, daré un script para construir e instalar MXNet 1.3.1 para Scala 2.12. * Y comentaré los puntos principales.

mxnet_2_12.sh


 #!/bin/sh #   MXNet     MXNET_VERSION=${1} #     ++  MXNet     MXNET_BUILD_OPT=${2} #       CUDA     CUDA_STUBS_DIR=${3} LD_LIBRARY_PATH="${LD_LIBRARY_PATH}:${CUDA_STUBS_DIR}" #       MXNet   apt-get install -y git build-essential libopenblas-dev libopencv-dev maven cmake && \ git clone -b ${MXNET_VERSION} --recursive https://github.com/dmlc/mxnet /tmp/mxnet && \ cd /tmp/mxnet && \ make -j $(nproc) ${MXNET_BUILD_OPT} && \ ln -s ${CUDA_STUBS_DIR}/libcuda.so ${CUDA_STUBS_DIR}/libcuda.so.1 && \ sed -rim 's/([a-zA-Z])_2.11/\1_2.12/g' $(find scala-package -name pom.xml) && \ sed -im 's/SCALA_VERSION_PROFILE := scala-2.11/SCALA_VERSION_PROFILE := scala-2.12/g' Makefile && \ sed -im 's/<module>spark<\/module>/<\!--<module>spark<\/module>-->/g' scala-package/pom.xml && \ make scalapkg ${MXNET_BUILD_OPT} && \ mkdir -p /usr/local/share/mxnet/scala/linux-x86_64-gpu && \ mv /tmp/mxnet/scala-package/assembly/linux-x86_64-gpu/target/mxnet-full_2.12-linux-x86_64-gpu-${MXNET_VERSION}-SNAPSHOT.jar /usr/local/share/mxnet/scala/linux-x86_64-gpu/mxnet-full_2.12-linux-x86_64-gpu-${MXNET_VERSION}-SNAPSHOT.jar && \ rm -rf /tmp/mxnet && rm -rf /root/.m2 

La parte más interesante comienza con esta línea:

 ln -s ${CUDA_STUBS_DIR}/libcuda.so ${CUDA_STUBS_DIR}/libcuda.so.1 && \ 

Si ejecuta el ensamblaje MXNet como se indica en las instrucciones, obtendremos un error. El compilador no puede encontrar la biblioteca libcuda.so.1, por lo que enlazaremos desde la biblioteca libcuda.so a libcuda.so.1. Es posible que esto no le moleste, cuando lo inicie en un servidor de producción, reemplazaremos esta biblioteca por una local. También tenga en cuenta que la ruta a las bibliotecas CUDA desde la variable de entorno CUDA_STUBS_DIR se ha agregado a LD_LIBRARY_PATH . Si esto no se hace, entonces el ensamblaje también fallará.

En estas líneas, reemplazamos la versión de Scala 2.11 con 2.12 en todos los archivos necesarios usando una expresión regular, que se seleccionó experimentalmente, porque no es suficiente reemplazar 2.11 en todas partes con 2.12:

 sed -rim 's/([a-zA-Z])_2.11/\1_2.12/g' $(find scala-package -name pom.xml) && \ sed -im 's/SCALA_VERSION_PROFILE := scala-2.11/SCALA_VERSION_PROFILE := scala-2.12/g' Makefile && \ sed -im 's/<module>spark<\/module>/<\!--<module>spark<\/module>-->/g' scala-package/pom.xml && \ make scalapkg ${MXNET_BUILD_OPT} && \ 

Y luego se comenta la dependencia del módulo para trabajar con Spark. Si esto no se hace, la biblioteca no se ensamblará.

Luego, ejecute el ensamblaje, como se indica en las instrucciones, copie la biblioteca ensamblada en una carpeta compartida y elimine cualquier basura que Maven haya bombeado durante el proceso de compilación (si esto no se hace, la imagen final crecerá en aproximadamente 3-4 GB, lo que puede causar que sus DevOps s nervioso).

Recopilamos la imagen, estando en el directorio raíz del proyecto (ver. Estructura de carpetas):

 your@pc$ docker build -f Dockerfile.2.12-1.3.1-9-7-runtime -t entony/scala-mxnet-cuda-cudnn:2.12-1.3.1-9-7-runtime . 

Permíteme recordarte por si acaso el punto al final dice que estamos haciendo el ensamblaje dentro del contexto del directorio actual.

Ahora es el momento de hablar sobre la imagen de construcción.

Dockerfile.2.12-1.3.1-9-7-builder


 #         runtime-,    FROM entony/scala-mxnet-cuda-cudnn:2.12-1.3.1-9-7-runtime #    ENV SCALA_VERSION 2.12.7 ENV SBT_VERSION 1.2.6 #     COPY install /install RUN chmod +x -R /install/* #       RUN apt-get update && \ cd /install && \ ./scala.sh ${SCALA_VERSION} && \ ./sbt.sh ${SBT_VERSION} #   RUN rm -rf /install 

Es simple, no necesitamos Scala y Sbt para iniciar nuestro microservicio, por lo que no tiene sentido arrastrarlos a la imagen base para el lanzamiento. Por lo tanto, crearemos una imagen separada que se usará solo para el ensamblaje. Los scripts scala.sh y sbt.sh son bastante triviales y no me detendré en ellos en detalle.

scala.sh


 #!/bin/sh #   Scala     SCALA_VERSION=${1} SCALA_DEB="http://www.scala-lang.org/files/archive/scala-${SCALA_VERSION}.deb" #  Scala apt-get install -y wget && \ wget -q ${SCALA_DEB} -O /tmp/scala.deb && dpkg -i /tmp/scala.deb && \ scala -version && \ rm /tmp/scala.deb 

sbt.sh


 #!/bin/sh #   Sbt     SBT_VERSION=${1} SBT_DEB="http://dl.bintray.com/sbt/debian/sbt-${SBT_VERSION}.deb" #  Sbt apt-get install -y wget && \ wget -q ${SBT_DEB} -O /tmp/sbt.deb && dpkg -i /tmp/sbt.deb && \ sbt sbtVersion && \ rm /tmp/sbt.deb 

Recopilamos la imagen, estando en el directorio raíz del proyecto (ver. Estructura de carpetas):

 your@pc$ docker build -f Dockerfile.2.12-1.3.1-9-7-builder -t entony/scala-mxnet-cuda-cudnn:2.12-1.3.1-9-7-builder . 

Al final del artículo hay enlaces al repositorio con todos estos archivos.

4. Estructura del proyecto.


Una vez que haya terminado de prepararse para el montaje del proyecto, hagamos lo que decidió dedicar tiempo a este artículo.

El proyecto de nuestro microservicio tendrá la siguiente estructura:

 \ | ----- dependencies | | ----- mxnet-full_2.12-linux-x86_64-gpu-1.3.1-SNAPSHOT.jar | ----- models | | ----- resnet50_ssd_model-0000.params | | ----- resnet50_ssd_model-symbol.json | | ----- synset.txt | ----- project | | ----- build.properties | | ----- plugins.sbt | ----- src | | ----- main | | | ----- resources | | | | ----- cat_and_dog.jpg | | | ----- scala | | | | ----- simple.predictor | | | | ----- Config | | | | ----- Model | | | | ----- Server | | | | ----- Run | | ----- test | | | ----- scala | | | | ----- simple.predictor | | | | ----- ServerTest | ----- build.sbt | ----- Dockerfile 

Esta es la estructura estándar de un proyecto Scala, con la excepción de las dependencias y modelos de directorios.
El directorio de dependencias contiene la biblioteca MXNet para Scala. Se puede obtener de dos maneras:

  • construya MXNet en la máquina donde va a desarrollar (tenga en cuenta que la biblioteca no es multiplataforma; si la construye en Linux, no funcionará en Mac OS),
  • o sáquelo de la imagen de Docker que creamos anteriormente. Si decide construir MXNet en un entorno local, el script mxnet_2.12.sh lo ayudará.

Puede extraer bibliotecas de la imagen de Docker de esta manera:

 #   your@pc$ mkdir dependencies #  Docker-    your@pc$ docker run -it --rm -v $(pwd)/dependencies:/tmp/dependencies entony/scala-mxnet-cuda-cudnn:2.12-1.3.1-9-7-runtime #          ab38e73d93@root$ cp /usr/local/share/mxnet/scala/linux-x86_64-gpu/mxnet-full_2.12-linux-x86_64-gpu-1.3.1-SNAPSHOT.jar /tmp/dependencies/mxnet-full_2.12-linux-x86_64-gpu-1.3.1-SNAPSHOT.jar ab38e73d93@root$ exit #  , ! your@pc$ ls dependencies/ mxnet-full_2.12-linux-x86_64-gpu-1.3.1-SNAPSHOT.jar 

El directorio de modelos contiene los archivos de una red neuronal entrenada, puede descargarlos libremente de la siguiente manera:

 #   your@pc$ mkdir models #     your@pc$ wget https://s3.amazonaws.com/model-server/models/resnet50_ssd/resnet50_ssd_model-symbol.json -P models your@pc$ wget https://s3.amazonaws.com/model-server/models/resnet50_ssd/resnet50_ssd_model-0000.params -P models your@pc$ wget https://s3.amazonaws.com/model-server/models/resnet50_ssd/synset.txt -P models 

Más brevemente sobre archivos que no son de particular interés, pero que juegan un papel en el proyecto.

project / build.properties


 #   Sbt,   sbt.version = 1.2.6 

proyecto / plugins.sbt


 //    sbt-pack addSbtPlugin("org.xerial.sbt" % "sbt-pack" % "0.12") 

src / main / resources / cat_and_dog.jpg


Una imagen tan maravillosa, en la que nuestra red neuronal buscará un gato y un perro.


build.sbt


 enablePlugins(PackPlugin) name := "simple-predictor" version := "0.1" scalaVersion := "2.12.7" unmanagedBase := baseDirectory.value / "dependencies" //  (   ) libraryDependencies ++= Seq( "org.json4s" %% "json4s-native" % "3.6.1", "org.scalatest" %% "scalatest" % "3.0.5" % Test, "org.scalaj" %% "scalaj-http" % "2.4.1" % Test ) //       packMain := Map("simple-predictor" -> "simple.predictor.Runs") //    bat-,      ,   Linux packGenerateWindowsBatFile := false //    JVM packJvmOpts := Map("simple-predictor" -> Seq( "-Xms3g", "-Xmx5g")) 

simple.predictor.Config


Este objeto almacena variables globales cuyo valor se lee de las variables de entorno o se establece de forma predeterminada.

 package simple.predictor import org.apache.mxnet.Context import scala.util.Try object Config { //    REST API val host: String = env("REST_HOST") getOrElse "0.0.0.0" //    REST API val port: Int = env("REST_PORT") flatMap (p => Try(p.toInt).toOption) getOrElse 8080 // URL,     POST-   val entryPoint: String = env("REST_ENTRY_POINT") getOrElse "/predict" //  ,       val threshold: Float = env("PROBABILITY_MORE") flatMap (p => Try(p.toFloat).toOption) getOrElse 0.5f //        val modelPrefix: String = env("MODEL_PREFIX") getOrElse "models/resnet50_ssd_model" //    (    ...-0000.params) val modemEpoch: Int = env("MODEL_EPOCH") flatMap (p => Try(p.toInt).toOption) getOrElse 0 //   ,     ,    512 val modemEdge: Int = env("MODEL_EDGE") flatMap (p => Try(p.toInt).toOption) getOrElse 512 //  ,   CPU ( ).  production  GPU val context: Context = env("MODEL_CONTEXT_GPU") flatMap { isGpu => Try(if (isGpu.toBoolean) Context.gpu() else Context.cpu()).toOption } getOrElse Context.cpu() private def env(name: String) = Option(System.getenv(name)) } 

simple.predictor.Run


El objeto Ejecutar es el punto de entrada a la aplicación.

 package simple.predictor import java.net.InetSocketAddress //     import simple.predictor.Config._ object Run extends App { //     REST- val model = new Model(modelPrefix, modemEpoch, modemEdge, threshold, context) val server = new Server(new InetSocketAddress(host, port), entryPoint, model) //   Ctrl + C    Runtime.getRuntime.addShutdownHook(new Thread(() => server.stop())) //      try server.start() catch { case ex: Exception => ex.printStackTrace() } } 

5. Carga de la red neuronal


La red neuronal se carga en el constructor de la clase simple.predictor.Model .

simple.predictor.Model


 package simple.predictor import java.awt.image.BufferedImage import org.apache.mxnet._ import org.apache.mxnet.infer.ObjectDetector import simple.predictor.Model.Prediction class Model(prefix: String, epoch: Int, imageEdge: Int, threshold: Float, context: Context) { //       val initShape = Shape(1, 3, imageEdge, imageEdge) val initData = DataDesc(name = "data", initShape, DType.Float32, Layout.NCHW) //           val model = new ObjectDetector(prefix, IndexedSeq(initData), context, Option(epoch)) //         ,       JSON private def toPrediction(originWidth: Int, originHeight: Int)(predict: (String, Array[Float])): Prediction = { val (objectClass, Array(probability, kx, ky, kw, kh)) = predict //        val x = (originWidth * kx).toInt val y = (originHeight * ky).toInt val w = (originWidth * kw).toInt val h = (originHeight * kh).toInt val width = if ((x + w) < originWidth) w else originWidth - x val height = if (y + h < originHeight) h else originHeight - y Prediction(objectClass, probability, x, y, width, height) } //     ,         ,     threshold def predict(image: BufferedImage): Seq[Prediction] = model.imageObjectDetect(image).head map toPrediction(image.getWidth, image.getHeight) filter (_.probability > threshold) } object Model { //   case class Prediction(objectClass: String, probability: Float, x: Int, y: Int, width: Int, height: Int) } 

En la sección dice que en una red neuronal funcionará con NDArray con una dimensión de 1 x 3 x 512 x 512, donde 1 es el número de imágenes que se incluirán en NDArray, 3 es el número de colores y 512 x 512 - tamaño de imagen (el valor de imageEdge = 12 se establece en el objeto simple.predict.Config , este es el tamaño lateral de la imagen utilizada para entrenar la red neuronal). Toda esta descripción de datos se pasa al ObjectDetector .

Otra sección interesante es el .

Después de ejecutar la imagen a través de la red neuronal, el resultado es del tipo Seq[Seq[(String, Array[Float])]] . La primera colección contiene solo un resultado (el formato de datos está determinado por una red neuronal específica), luego cada elemento de la siguiente colección es una tupla de dos elementos:

  1. nombre de clase ("gato", "perro", ...),
  2. una matriz de cinco números de coma flotante: el primero es la probabilidad, el segundo es el coeficiente para calcular la coordenada x , el tercero es el coeficiente para calcular la coordenada y , el cuarto es el coeficiente para calcular el ancho del rectángulo y el quinto es el coeficiente para calcular la altura del rectángulo.

Para obtener los valores reales de las coordenadas y las dimensiones del rectángulo, debe multiplicar el ancho y la altura originales de la imagen por los coeficientes correspondientes.
Me permito un poco de digresión sobre el tema de NDArray . Esta es una matriz multidimensional que MXNet crea en un contexto dado (CPU o GPU). Al crear un NDArray, se forma un objeto C ++, un objeto con el que las operaciones se realizan muy rápidamente (y si se crea en un contexto de GPU, es casi instantáneo), pero hay que pagar por esa velocidad. Como resultado (al menos en la versión MXNet 1.3.1) necesita administrar de forma independiente la memoria asignada para NDArray , y no olvide descargar estos objetos de la memoria después de que termine de trabajar con ellos. De lo contrario, habrá una pérdida de memoria significativa y bastante rápida, que no es muy conveniente de monitorear, ya que los programas para la creación de perfiles JVM no lo ven. El problema de memoria se agrava si trabaja en un contexto de GPU, ya que las tarjetas de video no tienen una gran cantidad de memoria y la aplicación se bloquea rápidamente.

¿Cómo resolver un problema de pérdida de memoria?

En el ejemplo anterior, en la línea model.imageObjectDetect(image).head map toPrediction(image.getWidth, image.getHeight) filter (_.probability > threshold) , el método imageObjectDetect se utiliza para ejecutar la imagen a través de la red neuronal, que recibe una entrada de BufferedImage . Todas las conversiones hacia y desde NDArray se realizan dentro del método, y no necesita pensar en problemas de desasignación de memoria. Por otro lado, antes de convertir BufferedImage a NDArray se realiza a un tamaño de 512 x 512 y la imagen se normaliza utilizando métodos de un objeto de tipo BufferedImage . Esto sucede un poco más que cuando se usa OpenCV, por ejemplo, pero resuelve el problema de liberar memoria después de usar NDArray .

Por supuesto, puede usar OpenCV y controlar la memoria usted mismo, para esto solo necesita llamar al método de dispose de dispose , pero por alguna razón olvidó mencionar esto en la documentación oficial de MXNet para Scala.

MXNet también tiene una forma no muy conveniente de controlar la pérdida de memoria que ocurre debido a NDArray . Para hacer esto, ejecute la aplicación con el parámetro JVM Dmxnet.traceLeakedObjects=true . Si MXNet nota un NDArray que no está en uso pero está colgado en la memoria, obtendrá una excepción que indica en qué línea de código está el NDArray desafortunado.

Mi consejo: trabaje directamente con NDArray, monitoree cuidadosamente la memoria y escriba la normalización usted mismo, habiendo especificado previamente qué algoritmo hizo el ingeniero de ML al entrenar una red neuronal, de lo contrario los resultados serán completamente diferentes. ObjectDetector tiene un método objectDetectWithNDArray al que puede pasar un NDArray . Para implementar un enfoque más universal para cargar una red neuronal, recomiendo usar el objeto org.apache.mxnet.module.Module . A continuación se muestra un ejemplo de uso.

 import org.apache.mxnet._ import org.apache.mxnet.io.NDArrayIter //      val model: Module = { val model = Module.loadCheckpoint(modelPrefix, modelEpoch, contexts = contexts) model.bind( forTraining = false, inputsNeedGrad = false, forceRebind = false, dataShape = DataDesc(name = "data", Shape(1, 3, 512, 512), DType.Float32, Layout.NCHW)) model.initParams() model } // NDArray  1  3  512  512 val image: NDArray = ??? //  dataBatch      val iterator = new NDArrayIter(IndexedSeq(image)) val dataBatch = iterator.next() image.dispose() //   val result: Seq[Array[Float]] = model.predict(dataBatch) map { ndArray => val array = ndArray.toArray ndArray.dispose() array } dataBatch.dispose() 

6. Implementación de la API REST


La clase simple.predictor.Server es responsable de implementar la API REST. El servidor se basa en el servidor Java incluido en Java.

simple.predictor.Server


 package simple.predictor import java.net.InetSocketAddress import com.sun.net.httpserver.{HttpExchange, HttpServer} import javax.imageio.ImageIO import org.json4s.DefaultFormats import org.json4s.native.Serialization class Server(address: InetSocketAddress, entryPoint: String, model: Model) { //   HTTP-,     java private val server = HttpServer.create(address, 0) //      URL server.createContext(entryPoint, (http: HttpExchange) => { //   HTTP-     val header = http.getRequestHeaders val (httpCode, json) = if (header.containsKey("Content-Type") && header.getFirst("Content-Type") == "image/jpeg") { //          ,      200 val image = ImageIO.read(http.getRequestBody) val predictionSeq = model.predict(image) (200, Map("prediction" -> predictionSeq)) } else (400, Map("error" -> "Invalid content")) //       400 //    JSON    val responseJson = Serialization.write(json)(DefaultFormats) val httpOs = http.getResponseBody http.getResponseHeaders.set("Content-Type", "application/json") http.sendResponseHeaders(httpCode, responseJson.length) httpOs.write(responseJson.getBytes) httpOs.close() }) def start(): Unit = server.start() def stop(): Unit = server.stop(0) } 

7. Prueba


Para verificar, inicie el servidor y envíe una imagen de prueba src / main / resources / cat_and_dog.jpg. Analizaremos el JSON recibido del servidor, verificaremos cuántos y qué objetos encontró la red neuronal en la imagen, y rodearemos los objetos en la imagen.

simple.predictor.ServerTest


 package simple.predictor import java.awt.{BasicStroke, Color, Font} import java.awt.image.BufferedImage import java.io.{ByteArrayOutputStream, File} import java.net.InetSocketAddress import javax.imageio.ImageIO import org.scalatest.{FlatSpec, Matchers} import scalaj.http.Http import org.json4s.{DefaultFormats, Formats} import org.json4s.native.JsonMethods.parse import simple.predictor.Config._ import simple.predictor.Model.Prediction import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global class ServerTest extends FlatSpec with Matchers { implicit val formats: Formats = DefaultFormats "Service" should "find a cat and a dog on photo" in { //      val model = new Model(modelPrefix, modemEpoch, modemEdge, threshold, context) val server = new Server(new InetSocketAddress(host, port), entryPoint, model) //      Future(server.start()) Thread.sleep(5000) //         val image = ImageIO.read(getClass.getResourceAsStream("/cat_and_dog.jpg")) val byteOS = new ByteArrayOutputStream() ImageIO.write(image, "jpg", byteOS) val data = byteOS.toByteArray //      ,     200 val response = Http(s"http://$host:$port$entryPoint").header("Content-Type", "image/jpeg").postData(data).asString response.code shouldEqual 200 //  JSON-, ,       val prediction = parse(response.body) \\ "prediction" prediction.children.size shouldEqual 2 //     , ,     ,    val objectClassList = (prediction \\ "objectClass").children map (_.extract[String]) objectClassList.head shouldEqual "cat" objectClassList.tail.head shouldEqual "dog" //   ,   val bBoxCoordinates = prediction.children.map(_.extract[Prediction]) //   ,     val imageWithBoundaryBoxes = new BufferedImage(image.getWidth, image.getHeight, image.getType) val graph = imageWithBoundaryBoxes.createGraphics() graph.drawImage(image, 0, 0, null) graph.setColor(Color.RED) graph.setStroke(new BasicStroke(5)) graph.setFont(new Font(Font.SANS_SERIF, Font.TRUETYPE_FONT, 30)) bBoxCoordinates foreach { case Prediction(obj, prob, x, y, width, height) => graph.drawRect(x, y, width, height) graph.drawString(s"$obj, prob: $prob", x + 15, y + 30) } graph.dispose() //         ImageIO.write(imageWithBoundaryBoxes, "jpg", new File("./test.jpg")) } } 

, .



8.


, . Docker , .

Dockerfile


 #       Sbt FROM entony/scala-mxnet-cuda-cudnn:2.12-1.3.1-9-7-builder AS builder #       RUN mkdir /tmp/source /tmp/source/dependencies COPY project /tmp/source/project COPY src /tmp/source/src COPY build.sbt /tmp/source/build.sbt #     MXNet,       RUN ln -s /usr/local/share/mxnet/scala/linux-x86_64-gpu/mxnet-full_2.12-linux-x86_64-gpu-1.3.1-SNAPSHOT.jar /tmp/source/dependencies/mxnet-full_2.12-linux-x86_64-gpu-1.3.1-SNAPSHOT.jar && \ cd /tmp/source/ && sbt pack #      FROM entony/scala-mxnet-cuda-cudnn:2.12-1.3.1-9-7-runtime #   LD   Cuda   Java ENV LD_LIBRARY_PATH /usr/local/cuda-9.0/targets/x86_64-linux/lib/stubs:/usr/local/share/OpenCV/java #            /opt/app/models ENV MODEL_PREFIX "/opt/app/models/resnet50_ssd_model" #            RUN mkdir -p /opt/app COPY --from=builder --chown=root:root /tmp/source/target/pack /opt/app COPY models /opt/app/models #      ENTRYPOINT /opt/app/bin/simple-predictor 



 # ,    ,   Dockerfile your@pc$ docker build -f Dockerfile -t entony/simple-predictor:1.0.0 . #   docker hub your@pc$ docker push entony/simple-predictor:1.0.0 

9. production- GPU


, docker hub, Nvidia, 8080 Docker, Cuda 9.0 Cudnn 7.

 #     Docker hub your@server-with-gpu$ docker pull entony/simple-predictor:1.0.0 #     your@server-with-gpu$ docker run -d \ -p 8080:8080 \ -e MODEL_CONTEXT_GPU=true \ -e MXNET_CUDNN_AUTOTUNE_DEFAULT=0 \ --name 'simple_predictor' \ --device /dev/nvidia0:/dev/nvidia0 \ --device /dev/nvidiactl:/dev/nvidiactl \ --device /dev/nvidia-uvm:/dev/nvidia-uvm \ -v /usr/lib/x86_64-linux-gnu/libcuda.so.1:/usr/local/cuda-9.0/targets/x86_64-linux/lib/stubs/libcuda.so.1:ro \ -v /usr/lib/nvidia-396/libnvidia-fatbinaryloader.so.396.54:/usr/local/cuda-9.0/targets/x86_64-linux/lib/stubs/libnvidia-fatbinaryloader.so.396.54:ro \ entony/simple-predictor:1.0.0 

Docker- --device Cuda- -v .

MODEL_CONTEXT_GPU GPU-, MXNET_CUDNN_AUTOTUNE_DEFAULT ( , , , ).

:

 #  your@server-with-gpu$ curl -X POST -H 'Content-Type: image/jpeg' --data-binary '@src/main/resources/cat_and_dog.jpg' http://0.0.0.0:8080/predict #  { "prediction":[ { "objectClass":"cat", "probability":0.9959417, "x":72,"y":439, "width":950, "height":987 }, { "objectClass":"dog", "probability":0.81277525, "x":966, "y":100, "width":870, "height":1326 } ] } 

Conclusión


MXNet , - . , , , production.

, , MXNet , Python production Scala, Java ++.

, .

, . . Gracias por su atencion

Referencias


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


All Articles