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 Problems2. Verwendete Technologien3. Vorbereiten eines einfachen Docker-Containers4. Projektstruktur5. Laden des neuronalen Netzes6. Implementierung der REST-API7. Testen8. Zusammenstellen eines Mikrodienstes basierend auf einem Basisbild9. Starten eines Microservices auf einem Produktionsserver mit einer GPUFazitReferenzen1. 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
java8.sh
opencv.sh
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
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
sbt.sh
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:
Das Modellverzeichnis enthält die Dateien eines trainierten neuronalen Netzwerks. Sie können sie wie folgt kostenlos herunterladen:
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
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"
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 {
simple.predictor.Run
Das Run-Objekt ist der Einstiegspunkt in die Anwendung.
package simple.predictor import java.net.InetSocketAddress
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) {
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:
- Klassenname ("Katze", "Hund", ...),
- 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
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) {
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 {
, .

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
9. production- GPU
, docker hub, Nvidia, 8080 Docker, Cuda 9.0 Cudnn 7.
Docker-
--device
Cuda-
-v
.
MODEL_CONTEXT_GPU
GPU-,
MXNET_CUDNN_AUTOTUNE_DEFAULT
( , , , ).
:
Fazit
MXNet , - . , , , production.
, , MXNet , Python production Scala, Java ++.
, .
, . . Vielen Dank für Ihre Aufmerksamkeit.
Referenzen