Scala + MXNet = Microservice avec neurone en prod


Sur Internet, il existe un grand nombre de manuels et d'exemples, sur la base desquels, chers lecteurs, vous pourrez écrire du code «sans trop de difficulté» et avec un temps «minimal» permettant de distinguer les chats des chiens sur une photo. Et pourquoi alors perdre du temps sur cet article?

À mon avis, le principal inconvénient de tous ces exemples réside dans les possibilités limitées. Vous avez pris un exemple - même avec le réseau neuronal de base que l'auteur propose - l'a lancé, peut-être même que cela a fonctionné, et ensuite? Comment faire fonctionner ce code simple sur un serveur de production? Comment le mettre à jour et le maintenir? C'est là que le plaisir commence. Je n'ai pas pu trouver une description complète du processus à partir du moment «Eh bien, l'ingénieur ML a formé le réseau neuronal» à «finalement nous l'avons déployé en production». Et j'ai décidé de combler cet écart.

Je ne parlerai pas de la façon d'enseigner au réseau neuronal de nouvelles choses amusantes qui vous plairont et vous aideront à gagner un tas de billets croustillants. C'est un excellent sujet pour un article séparé. À titre d'exemple, j'utiliserai un réseau de neurones qui peut être téléchargé gratuitement. La tâche principale que je me suis fixée est de donner une description complète du processus d'introduction d'un réseau neuronal en fonctionnement.

Je réponds immédiatement à la question «Pourquoi pas en Python?»: Nous utilisons Scala pour les solutions de production en raison de l'écriture plus pratique et stable du code multi-thread.

Table des matières


1. Énoncé du problème
2. Technologies utilisées
3. Préparation d'un conteneur docker de base
4. Structure du projet
5. Chargement du réseau neuronal
6. Implémentation de l'API REST
7. Test
8. Assemblage d'un microservice basé sur une image de base
9. Démarrage d'un microservice sur un serveur de production avec un GPU
Conclusion
Les références

1. Énoncé du problème


Supposons que nous ayons une grande base de données de photos avec différents objets et que nous devons créer un microservice qui recevra une image dans une demande HTTP POST et répondra au format JSON. La réponse doit contenir le nombre d'objets trouvés et leurs classes, le degré de probabilité qu'il s'agisse exactement de l'objet de la classe déclarée et les coordonnées des rectangles couvrant les limites de chaque objet.

2. Technologies utilisées


  • Scala 2.12.7 + ensemble minimum de bibliothèques supplémentaires, Sbt 1.2.6 avec le plugin Sbt-pack 0.12 pour construire des codes sources.
  • MXNet 1.3.1 (la dernière version stable au moment de la rédaction), compilé pour Scala 2.12.
  • Serveur avec cartes graphiques Nvidia.
  • Cuda 9.0 et Cudnn 7 installés sur le serveur.
  • Java 8 pour exécuter du code compilé.
  • Docker pour faciliter l'assemblage, la livraison et le lancement du microservice sur le serveur.

3. Préparation d'un conteneur docker de base


Pour notre microservice, vous aurez besoin d'une image Docker de base dans laquelle le nombre minimum de dépendances nécessaires à l'exécution sera installé. Pour l'assemblage, nous utiliserons l'image avec Sbt installé en plus. Oui, nous construirons les sources elles-mêmes non pas dans l'environnement local, mais dans le conteneur Docker. Cela facilitera la transition vers l'assemblage via CI, par exemple, via gitlab CI.

Structure des dossiers:

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


Cette image sera utilisée pour le lancement final du microservice. Il est basé sur l'image officielle de Nvidia avec CUDA 9.0 et CUDNN 7. préinstallés. La documentation de MXNet 1.3.1 prétend fonctionner avec CUDA 8.0, mais, comme la pratique l'a montré, tout fonctionne bien avec la version 9.0, et même un peu plus vite.

De plus, nous installerons Java 8, MXNet 1.3.1 (nous le construirons sous Scala 2.12), OpenCV 3.4.3 et l'utilitaire Linux pour définir le fuseau horaire dans cette image.

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

Les scripts timeZone.sh java8.sh et opencv.sh sont assez triviaux, donc je ne m'attarderai pas sur eux en détail, ils sont présentés ci-dessous.

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} 

L'installation de MXNet n'est pas si simple. Le fait est que tous les assemblages de cette bibliothèque pour Scala sont faits sur la base de la version 2.11 du compilateur, et cela est justifié, car la bibliothèque comprend un module pour travailler avec Spark, qui, à son tour, est écrit en Scala 2.11. Étant donné que nous utilisons Scala 2.12.7 en développement, les bibliothèques compilées ne nous conviennent pas et nous ne pouvons pas passer à la version 2.11. * Nous ne pouvons pas, en raison de la grande quantité de code déjà écrit sur la nouvelle version de Scala. Que faire Obtenez beaucoup de plaisir à collecter MXNet à partir des sources pour notre version de Scala. Ci-dessous, je donnerai un script pour construire et installer MXNet 1.3.1 pour Scala 2.12. * Et commenter les points principaux.

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 partie la plus intéressante commence par cette ligne:

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

Si vous exécutez l'assemblage MXNet comme dans les instructions, nous obtiendrons une erreur. Le compilateur ne peut pas trouver la bibliothèque libcuda.so.1, nous lierons donc la bibliothèque libcuda.so à libcuda.so.1. Cela peut ne pas vous déranger, lorsque vous le démarrez sur un serveur de production, nous remplacerons cette bibliothèque par une locale. Notez également que le chemin d'accès aux bibliothèques CUDA à partir de la variable d'environnement CUDA_STUBS_DIR a été ajouté à LD_LIBRARY_PATH . Si cela n'est pas fait, l'assemblage échouera également.

Dans ces lignes, nous remplaçons la version de Scala 2.11 par 2.12 dans tous les fichiers nécessaires en utilisant une expression régulière, qui a été sélectionnée expérimentalement, car il ne suffit pas de remplacer simplement 2.11 partout par 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} && \ 

Et puis la dépendance du module pour travailler avec Spark est commentée. Si cela n'est pas fait, la bibliothèque ne sera pas assemblée.

Ensuite, exécutez l'assembly, comme indiqué dans les instructions, copiez la bibliothèque assemblée dans un dossier partagé et supprimez toutes les ordures que Maven a pompées pendant le processus de construction (si cela n'est pas fait, l'image finale augmentera d'environ 3-4 Go, ce qui peut entraîner votre DevOps s nerveux).

Nous collectons l'image, étant dans le répertoire racine du projet (voir. Structure des dossiers):

 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 . 

Permettez-moi de vous rappeler juste au cas où le point à la fin indique que nous faisons l'assemblage dans le contexte du répertoire actuel.

Il est maintenant temps de parler de l'image de construction.

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 

C'est simple, nous n'avons pas besoin de Scala et Sbt pour démarrer notre microservice, il est donc inutile de les faire glisser dans l'image de base pour le lancement. Par conséquent, nous allons créer une image distincte qui sera utilisée uniquement pour l'assemblage. Les scripts scala.sh et sbt.sh sont assez triviaux et je ne m'attarderai pas sur eux en détail.

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 

Nous collectons l'image, étant dans le répertoire racine du projet (voir. Structure des dossiers):

 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 . 

À la fin de l'article, il y a des liens vers le référentiel avec tous ces fichiers.

4. Structure du projet


Après avoir terminé la préparation de l'assemblage du projet, faisons ce que vous avez décidé de consacrer du temps à cet article.

Le projet de notre microservice aura la structure suivante:

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

Il s'agit de la structure standard d'un projet Scala, à l'exception des dépendances et des modèles de répertoires.
Le répertoire des dépendances contient la bibliothèque MXNet pour Scala. Il peut être obtenu de deux manières:

  • construire MXNet sur la machine sur laquelle vous allez développer (notez que la bibliothèque n'est pas multi-plateforme; si vous la construisez sur Linux, elle ne fonctionnera pas sur Mac OS),
  • ou retirez-le de l'image Docker que nous avons créée plus tôt. Si vous décidez de créer MXNet dans un environnement local, le script mxnet_2.12.sh vous aidera.

Vous pouvez extraire des bibliothèques de l'image Docker comme ceci:

 #   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 

Le répertoire des modèles contient les fichiers d'un réseau neuronal formé, vous pouvez les télécharger librement comme suit:

 #   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 

Plus brièvement sur les fichiers qui ne présentent pas d'intérêt particulier, mais qui jouent un rôle dans le projet.

project / build.properties


 #   Sbt,   sbt.version = 1.2.6 

project / plugins.sbt


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

src / main / resources / cat_and_dog.jpg


Une si belle image, dans laquelle notre réseau de neurones recherchera un chat et un chien.


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


Cet objet stocke des variables globales dont la valeur est lue à partir des variables d'environnement ou définie par défaut.

 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


L'objet Run est le point d'entrée de l'application.

 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. Chargement du réseau neuronal


Le réseau neuronal est chargé dans le constructeur de la classe 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) } 

Dans la section vous dites que dans un réseau de neurones, cela fonctionnera avec NDArray avec une dimension de 1 x 3 x 512 x 512, où 1 est le nombre d'images qui seront contenues dans NDArray, 3 est le nombre de couleurs et 512 x 512 - la taille de l'image (la valeur de imageEdge = 12 est définie dans l'objet simple.predict.Config , c'est la taille latérale de l'image utilisée pour entraîner le réseau neuronal). Toutes ces descriptions de données sont transmises à l' ObjectDetector .

Une autre section intéressante est le .

Après avoir exécuté l'image à travers le réseau neuronal, le résultat est du type Seq[Seq[(String, Array[Float])]] . La première collection ne contient qu'un seul résultat (le format des données est déterminé par un réseau neuronal spécifique), puis chaque élément de la collection suivante est un tuple de deux éléments:

  1. nom de classe ("chat", "chien", ...),
  2. un tableau de cinq nombres à virgule flottante: le premier est la probabilité, le second est le coefficient pour calculer la coordonnée x , le troisième est le coefficient pour calculer la coordonnée y , le quatrième est le coefficient pour calculer la largeur du rectangle, et le cinquième est le coefficient pour calculer la hauteur du rectangle.

Pour obtenir les valeurs réelles des coordonnées et des dimensions du rectangle, vous devez multiplier la largeur et la hauteur d'origine de l'image par les coefficients correspondants.
Je me permets une petite digression sur le sujet de NDArray . Il s'agit d'un tableau multidimensionnel que MXNet crée dans un contexte donné (CPU ou GPU). Lors de la création d'un NDArray, un objet C ++ est formé, un objet avec lequel les opérations sont effectuées très rapidement (et s'il est créé dans un contexte GPU, il est presque instantané), mais vous devez payer pour une telle vitesse. Par conséquent (au moins dans la version MXNet 1.3.1), vous devez gérer indépendamment la mémoire allouée à NDArray , et n'oubliez pas de décharger ces objets de la mémoire après avoir fini de travailler avec eux. Sinon, il y aura une fuite de mémoire importante et assez rapide, ce qui n'est pas très pratique à surveiller, car les programmes de profilage JVM ne le voient pas. Le problème de mémoire est aggravé si vous travaillez dans un contexte GPU, car les cartes vidéo n'ont pas une grande quantité de mémoire et l'application se bloque rapidement en mémoire.

Comment résoudre un problème de fuite de mémoire?

Dans l'exemple ci-dessus, dans la ligne model.imageObjectDetect(image).head map toPrediction(image.getWidth, image.getHeight) filter (_.probability > threshold) , la méthode imageObjectDetect est utilisée pour exécuter l'image via le réseau de neurones, qui reçoit une entrée BufferedImage . Toutes les conversions vers et depuis NDArray sont effectuées à l'intérieur de la méthode, et vous n'avez pas besoin de penser aux problèmes de désallocation de mémoire. D'un autre côté, avant de convertir BufferedImage en NDArray est effectué à une taille de 512 x 512 et l'image est normalisée à l'aide des méthodes d'un objet de type BufferedImage . Cela se produit un peu plus longtemps que lors de l'utilisation d'OpenCV, par exemple, mais cela résout le problème de la libération de mémoire après l'utilisation de NDArray .

Vous pouvez, bien sûr, utiliser OpenCV et contrôler la mémoire vous-même, pour cela, il vous suffit d'appeler la méthode d' dispose de dispose , mais pour une raison quelconque, vous avez oublié de le mentionner dans la documentation MXNet officielle de Scala.

MXNet a également un moyen peu pratique de contrôler la fuite de mémoire qui se produit en raison de NDArray . Pour ce faire, exécutez l'application avec le paramètre JVM Dmxnet.traceLeakedObjects=true . Si MXNet remarque un NDArray qui n'est pas utilisé mais se bloque en mémoire, vous obtiendrez une exception indiquant dans quelle ligne de code le NDArray malheureux NDArray .

Mon conseil: travaillez directement avec NDArray, surveillez attentivement la mémoire et écrivez vous-même la normalisation, après avoir spécifié quel algorithme l'ingénieur ML a fait lors de la formation d'un réseau de neurones, sinon les résultats seront complètement différents. ObjectDetector possède une méthode objectDetectWithNDArray à laquelle vous pouvez passer un NDArray . Pour implémenter une approche plus universelle du chargement d'un réseau de neurones, je recommande d'utiliser l'objet org.apache.mxnet.module.Module . Voici un exemple d'utilisation.

 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. Implémentation de l'API REST


La classe simple.predictor.Server est responsable de l'implémentation de l'API REST. Le serveur est basé sur le serveur Java inclus dans 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. Test


Pour vérifier, démarrez le serveur et envoyez une image de test src / main / resources / cat_and_dog.jpg. Nous analyserons le JSON reçu du serveur, vérifierons combien et quels objets le réseau neuronal a trouvés dans l'image et encerclons les objets dans l'image.

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 } ] } 

Conclusion


MXNet , - . , , , production.

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

, .

, . . Merci de votre attention.

Les références


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


All Articles