Scala + MXNet = Microservice mit Neuron in Prod


Es gibt eine Vielzahl von Handbüchern und Beispielen im Internet, auf deren Grundlage Sie, liebe Leser, „ohne große Schwierigkeiten“ und mit „minimalen“ Zeitkosten Code schreiben können, der Katzen von Hunden auf einem Foto unterscheiden kann. Und warum dann Zeit mit diesem Artikel verschwenden?

Der Hauptnachteil all dieser Beispiele sind meiner Meinung nach die begrenzten Möglichkeiten. Sie haben ein Beispiel genommen - selbst mit dem grundlegenden neuronalen Netzwerk, das der Autor anbietet - es gestartet, vielleicht hat es sogar funktioniert, und wie geht es weiter? Wie kann dieser einfache Code auf einem Produktionsserver ausgeführt werden? Wie aktualisiere und pflege ich es? Hier beginnt der Spaß. Ich konnte keine vollständige Beschreibung des Prozesses von dem Moment an finden, in dem „Nun, der ML-Ingenieur hat das neuronale Netzwerk trainiert“ bis „endlich haben wir es in die Produktion eingeführt“. Und ich habe beschlossen, diese Lücke zu schließen.

Ich werde nicht darüber sprechen, wie man dem neuronalen Netzwerk neue lustige Dinge beibringt, die Ihnen gefallen und Ihnen helfen, ein paar knusprige Banknoten zu verdienen. Dies ist ein großartiges Thema für einen separaten Artikel. Als Beispiel werde ich ein neuronales Netzwerk verwenden, das frei heruntergeladen werden kann. Die Hauptaufgabe, die ich mir gestellt habe, ist es, eine vollständige Beschreibung des Prozesses der Einführung eines neuronalen Netzwerks in Betrieb zu geben.

Ich beantworte sofort die Frage „Warum nicht in Python?“: Wir verwenden Scala für Produktionslösungen, da das Schreiben von Multithread-Code bequemer und stabiler ist.

Inhalt


1. Erklärung des Problems
2. Verwendete Technologien
3. Vorbereiten eines einfachen Docker-Containers
4. Projektstruktur
5. Laden des neuronalen Netzes
6. Implementierung der REST-API
7. Testen
8. Zusammenstellen eines Mikrodienstes basierend auf einem Basisbild
9. Starten eines Microservices auf einem Produktionsserver mit einer GPU
Fazit
Referenzen

1. Erklärung des Problems


Angenommen, wir haben eine große Datenbank mit Fotos mit verschiedenen Objekten und müssen einen Microservice erstellen, der ein Bild in einer HTTP-POST-Anforderung empfängt und im JSON-Format antwortet. Die Antwort sollte die Anzahl der gefundenen Objekte und ihre Klassen, den Grad der Wahrscheinlichkeit, dass dies genau das Objekt der deklarierten Klasse ist, und die Koordinaten der Rechtecke enthalten, die die Grenzen jedes Objekts abdecken.

2. Verwendete Technologien


  • Scala 2.12.7 + Mindestmenge an zusätzlichen Bibliotheken, Sbt 1.2.6 mit dem Sbt-Pack 0.12-Plugin zum Erstellen von Quellcodes.
  • MXNet 1.3.1 (die neueste stabile Version zum Zeitpunkt des Schreibens), kompiliert für Scala 2.12.
  • Server mit Nvidia-Grafikkarten.
  • Cuda 9.0 und Cudnn 7 sind auf dem Server installiert.
  • Java 8 zum Ausführen von kompiliertem Code.
  • Docker für die einfache Montage, Bereitstellung und den Start von Microservice auf dem Server.

3. Vorbereiten eines einfachen Docker-Containers


Für unseren Microservice benötigen Sie ein einfaches Docker-Image, in dem die Mindestanzahl der zum Ausführen erforderlichen Abhängigkeiten installiert ist. Für die Montage verwenden wir das Image mit zusätzlich installiertem Sbt. Ja, wir erstellen die Quellen selbst nicht in der lokalen Umgebung, sondern im Docker-Container. Dies erleichtert den weiteren Übergang zur Baugruppe über CI, beispielsweise über gitlab CI.

Ordnerstruktur:

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


Dieses Bild wird für den endgültigen Start des Microservices verwendet. Es basiert auf dem offiziellen Image von Nvidia mit vorinstalliertem CUDA 9.0 und CUDNN 7. Die Dokumentation für MXNet 1.3.1 behauptet, mit CUDA 8.0 zu funktionieren, aber wie die Praxis gezeigt hat, funktioniert alles gut mit Version 9.0 und sogar etwas schneller.

Darüber hinaus werden wir Java 8, MXNet 1.3.1 (wir werden es unter Scala 2.12 erstellen), OpenCV 3.4.3 und das Linux-Dienstprogramm zum Festlegen der Zeitzone in diesem Image installieren.

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

Die Skripte timeZone.sh java8.sh und opencv.sh sind ziemlich trivial, daher werde ich nicht näher darauf eingehen. Sie werden im Folgenden vorgestellt.

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} 

Die Installation von MXNet ist nicht so einfach. Tatsache ist, dass alle Assemblys dieser Bibliothek für Scala auf der Basis der Compiler-Version 2.11 erstellt wurden. Dies ist gerechtfertigt, da die Bibliothek ein Modul für die Arbeit mit Spark enthält, das wiederum in Scala 2.11 geschrieben ist. Da wir Scala 2.12.7 in der Entwicklung verwenden, sind die kompilierten Bibliotheken nicht für uns geeignet und wir können nicht auf Version 2.11 zurückgreifen. * Dies ist aufgrund der großen Menge an Code, die bereits in der neuen Scala-Version geschrieben wurde, nicht möglich. Was zu tun ist? Holen Sie sich viel Spaß beim Sammeln von MXNet aus dem Quellcode für unsere Version von Scala. Im Folgenden werde ich ein Skript zum Erstellen und Installieren von MXNet 1.3.1 für Scala 2.12 geben. * Und die wichtigsten Punkte kommentieren.

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 

Der interessanteste Teil beginnt mit dieser Zeile:

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

Wenn Sie die MXNet-Assembly wie in der Anleitung beschrieben ausführen, wird eine Fehlermeldung angezeigt. Der Compiler kann die Bibliothek libcuda.so.1 nicht finden, daher verlinken wir von der Bibliothek libcuda.so zu libcuda.so.1. Dies stört Sie möglicherweise nicht. Wenn Sie es auf einem Produktionsserver starten, ersetzen wir diese Bibliothek durch eine lokale. Beachten Sie außerdem, dass der Pfad zu den CUDA-Bibliotheken aus der Umgebungsvariablen LD_LIBRARY_PATH zu LD_LIBRARY_PATH hinzugefügt wurde. Wenn dies nicht erfolgt, schlägt auch die Montage fehl.

In diesen Zeilen ersetzen wir die Version von Scala 2.11 durch 2.12 in allen erforderlichen Dateien unter Verwendung eines regulären Ausdrucks, der experimentell ausgewählt wurde, da es nicht ausreicht, 2.11 einfach überall durch 2.12 zu ersetzen:

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

Und dann wird die Abhängigkeit vom Modul für die Arbeit mit Spark kommentiert. Andernfalls wird die Bibliothek nicht zusammengestellt.

Führen Sie als Nächstes die Assembly aus, wie in den Anweisungen angegeben, kopieren Sie die zusammengestellte Bibliothek in einen freigegebenen Ordner und entfernen Sie den Müll, den Maven während des Erstellungsprozesses gepumpt hat. Andernfalls wächst das endgültige Image um ca. 3-4 GB, was zu Ihren DevOps führen kann s nervös).

Wir sammeln das Bild im Stammverzeichnis des Projekts (siehe Ordnerstruktur):

 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 . 

Ich möchte Sie nur für den Fall daran erinnern, dass der Punkt am Ende besagt, dass wir die Montage im Kontext des aktuellen Verzeichnisses durchführen.

Jetzt ist es Zeit, über das Build-Image zu sprechen.

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 ist ganz einfach, wir brauchen Scala und Sbt nicht, um unseren Microservice zu starten. Es macht also keinen Sinn, sie zum Start in das Basis-Image zu ziehen. Daher erstellen wir ein separates Image, das nur für die Montage verwendet wird. Die Skripte scala.sh und sbt.sh sind ziemlich trivial und ich werde nicht im Detail darauf eingehen.

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 

Wir sammeln das Bild im Stammverzeichnis des Projekts (siehe Ordnerstruktur):

 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 . 

Am Ende des Artikels befinden sich Links zum Repository mit all diesen Dateien.

4. Projektstruktur


Nachdem Sie die Vorbereitung für die Montage des Projekts abgeschlossen haben, lassen Sie uns das tun, wofür Sie sich entschieden haben, Zeit für diesen Artikel aufzuwenden.

Das Projekt unseres Microservices wird folgende Struktur haben:

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

Dies ist die Standardstruktur eines Scala-Projekts mit Ausnahme der Verzeichnisabhängigkeiten und -modelle.
Das Abhängigkeitsverzeichnis enthält die MXNet-Bibliothek für Scala. Es kann auf zwei Arten erhalten werden:

  • Erstellen Sie MXNet auf dem Computer, auf dem Sie entwickeln werden (beachten Sie, dass die Bibliothek nicht plattformübergreifend ist; wenn Sie sie unter Linux erstellen, funktioniert sie nicht unter Mac OS).
  • oder ziehen Sie es aus dem Docker-Image heraus, das wir zuvor erstellt haben. Wenn Sie MXNet in einer lokalen Umgebung erstellen möchten, hilft Ihnen das Skript mxnet_2.12.sh.

Sie können Bibliotheken wie folgt aus dem Docker-Image herausziehen:

 #   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 

Das Modellverzeichnis enthält die Dateien eines trainierten neuronalen Netzwerks. Sie können sie wie folgt kostenlos herunterladen:

 #   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 

Weitere kurze Informationen zu Dateien, die nicht von besonderem Interesse sind, aber im Projekt eine Rolle spielen.

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


So ein wunderbares Bild, in dem unser neuronales Netzwerk nach einer Katze und einem Hund suchen wird.


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


Dieses Objekt speichert globale Variablen, deren Wert aus Umgebungsvariablen gelesen oder standardmäßig festgelegt wird.

 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


Das Run-Objekt ist der Einstiegspunkt in die Anwendung.

 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. Laden des neuronalen Netzes


Das neuronale Netzwerk wird in den Konstruktor der Klasse simple.predictor.Model geladen.

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

Im Abschnitt sagen Sie, dass es in einem neuronalen Netzwerk mit NDArray mit einer Dimension von 1 x 3 x 512 x 512 NDArray , wobei 1 die Anzahl der in NDArray enthaltenen Bilder, 3 die Anzahl der Farben und 512 x 512 ist - Bildgröße (der Wert von imageEdge = 12 wird im Objekt simple.predict.Config . Dies ist die Seitengröße des Bildes, das zum Trainieren des neuronalen Netzwerks verwendet wird.) Alle diese Datenbeschreibungen werden an den ObjectDetector .

Ein weiterer interessanter Abschnitt ist die .

Nachdem das Bild durch das neuronale Netzwerk ausgeführt wurde, ist das Ergebnis vom Typ Seq[Seq[(String, Array[Float])]] . Die erste Sammlung enthält nur ein Ergebnis (das Datenformat wird durch ein bestimmtes neuronales Netzwerk bestimmt), dann ist jedes Element der nächsten Sammlung ein Tupel aus zwei Elementen:

  1. Klassenname ("Katze", "Hund", ...),
  2. ein Array von fünf Gleitkommazahlen: Die erste ist die Wahrscheinlichkeit, die zweite ist der Koeffizient zur Berechnung der x Koordinate, die dritte ist der Koeffizient zur Berechnung der y Koordinate, die vierte ist der Koeffizient zur Berechnung der Breite des Rechtecks ​​und die fünfte ist der Koeffizient zur Berechnung der Höhe des Rechtecks.

Um die tatsächlichen Werte der Koordinaten und Abmessungen des Rechtecks ​​zu erhalten, müssen Sie die ursprüngliche Breite und Höhe des Bildes mit den entsprechenden Koeffizienten multiplizieren.
Ich erlaube mir einen kleinen Exkurs zum Thema NDArray . Dies ist ein mehrdimensionales Array, das MXNet in einem bestimmten Kontext (CPU oder GPU) erstellt. Beim Erstellen eines NDArray wird ein C ++ - Objekt gebildet, ein Objekt, mit dem Operationen sehr schnell ausgeführt werden (und wenn es in einem GPU-Kontext erstellt wird, ist es fast augenblicklich), aber Sie müssen für diese Geschwindigkeit bezahlen. Daher müssen Sie (zumindest in Version MXNet 1.3.1) den für NDArray zugewiesenen Speicher unabhängig verwalten und vergessen nicht, diese Objekte nach Abschluss der Arbeit aus dem Speicher zu entladen. Andernfalls tritt ein erheblicher und relativ schneller Speicherverlust auf, der nicht sehr bequem zu überwachen ist, da Programme für die JVM-Profilerstellung ihn nicht sehen. Das Speicherproblem wird verschlimmert, wenn Sie in einem GPU-Kontext arbeiten, da Grafikkarten nicht über viel Speicher verfügen und die Anwendung schnell keinen Speicher mehr hat.

Wie löse ich ein Speicherverlustproblem?

Im obigen Beispiel wird in der Zeile model.imageObjectDetect(image).head map toPrediction(image.getWidth, image.getHeight) filter (_.probability > threshold) die imageObjectDetect Methode verwendet, um das Bild durch das neuronale Netzwerk zu führen, das eine BufferedImage Eingabe empfängt. Alle Konvertierungen von und zu NDArray werden innerhalb der Methode durchgeführt, und Sie müssen nicht über Probleme bei der Speicherfreigabe nachdenken. Andererseits wird vor dem Konvertieren von NDArray in NDArray bei einer Größe von 512 x 512 durchgeführt und das Bild unter Verwendung der Methoden eines Objekts vom Typ BufferedImage normalisiert. Dies geschieht etwas länger als beispielsweise bei Verwendung von OpenCV, löst jedoch das Problem der Speicherfreigabe nach Verwendung von NDArray .

Sie können natürlich OpenCV verwenden und den Speicher selbst steuern. Dazu müssen Sie nur die NDArray von dispose . Aus irgendeinem Grund haben Sie jedoch vergessen, dies in der offiziellen MXNet-Dokumentation für Scala zu erwähnen.

MXNet bietet auch eine nicht so bequeme Möglichkeit, den durch NDArray Speicherverlust zu NDArray . Führen Sie dazu die Anwendung mit dem JVM-Parameter Dmxnet.traceLeakedObjects=true . Wenn MXNet ein NDArray bemerkt, das nicht verwendet wird, aber im Speicher hängt, erhalten Sie eine Ausnahme, die angibt, in welcher Codezeile sich das unglückliche NDArray .

Mein Rat: Arbeiten Sie direkt mit NDArray, überwachen Sie den Speicher sorgfältig und schreiben Sie die Normalisierung selbst, nachdem Sie zuvor angegeben haben, welchen Algorithmus der ML-Ingenieur beim Training eines neuronalen Netzwerks verwendet hat. Andernfalls sind die Ergebnisse völlig anders. Der ObjectDetector verfügt über eine objectDetectWithNDArray Methode, an die Sie ein NDArray . Um einen universelleren Ansatz zum Laden eines neuronalen Netzwerks zu implementieren, empfehle ich die Verwendung des Objekts org.apache.mxnet.module.Module . Unten finden Sie ein Anwendungsbeispiel.

 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. Implementierung der REST-API


Die simple.predictor.Server Klasse ist für die Implementierung der REST-API verantwortlich. Der Server basiert auf dem in Java enthaltenen Java-Server.

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. Testen


Starten Sie zur Überprüfung den Server und senden Sie ein Testbild src / main / resources / cat_and_dog.jpg. Wir analysieren den vom Server empfangenen JSON, überprüfen, wie viele und welche Objekte das neuronale Netzwerk im Bild gefunden hat, und kreisen die Objekte im Bild ein.

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

Fazit


MXNet , - . , , , production.

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

, .

, . . Vielen Dank für Ihre Aufmerksamkeit.

Referenzen


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


All Articles