Scala + MXNet = Microservice مع neuron in prod


يوجد على شبكة الإنترنت عدد كبير من الأدلة والأمثلة ، والتي على أساسها ستتمكن ، أيها القراء الأعزاء ، من كتابة الكود "دون صعوبة كبيرة" وفي الوقت "الأدنى" الذي يميز القطط عن الكلاب في الصورة. ولماذا نضيع الوقت في هذا المقال؟

الرئيسية ، في رأيي ، العيب في كل هذه الأمثلة هو الاحتمالات المحدودة. لقد أخذت مثالاً - حتى مع الشبكة العصبية الأساسية التي يقدمها المؤلف - أطلقتها ، وربما نجحت ، وماذا بعد؟ كيفية جعل هذا الكود البسيط يبدأ العمل على خادم الإنتاج؟ كيفية تحديثها وصيانتها؟ هذا هو المكان الذي تبدأ فيه المتعة. لم أتمكن من العثور على وصف كامل للعملية منذ اللحظة "حسنًا ، قام مهندس ML بتدريب الشبكة العصبية" على "أخيرًا تم طرحها في الإنتاج". وقررت إغلاق هذه الفجوة.

لن أتحدث عن كيفية تعليم الشبكة العصبية أشياء مضحكة جديدة ترضيك وتساعدك على كسب مجموعة من الأوراق النقدية المقرمشة. هذا موضوع رائع لمقال منفصل. على سبيل المثال ، سأستخدم شبكة عصبية يمكن تنزيلها مجانًا. المهمة الرئيسية التي حددتها لنفسي هي تقديم وصف كامل لعملية إدخال شبكة عصبية في العملية.

أجب على الفور على السؤال "لماذا لا يوجد في Python؟": نحن نستخدم Scala لحلول الإنتاج بسبب الكتابة الأكثر ملاءمة وثباتًا للكود متعدد الخيوط.

المحتويات


1. بيان المشكلة
2. التقنيات المستخدمة
3. إعداد حاوية عامل ميناء الأساسية
4. هيكل المشروع
5. تحميل الشبكة العصبية
6. REST تنفيذ API
7. اختبار
8. تجميع خدمة microservice بناءً على صورة أساسية
9. بدء microservice على خادم الإنتاج مع GPU
الخاتمة
المراجع

1. بيان المشكلة


لنفترض أن لدينا قاعدة بيانات كبيرة من الصور ذات كائنات مختلفة ، ونحن بحاجة إلى إنشاء خدمة ميكروية ستتلقى صورة في طلب HTTP POST والرد بتنسيق JSON. يجب أن تحتوي الإجابة على عدد الكائنات التي تم العثور عليها وفئاتها ، ودرجة احتمال أن يكون هذا بالضبط هو كائن الفئة المعلنة ، وإحداثيات المستطيلات التي تغطي حدود كل كائن.

2. التقنيات المستخدمة


  • Scala 2.12.7 + مجموعة دنيا من المكتبات الإضافية ، Sbt 1.2.6 مع البرنامج المساعد Sbt-pack 0.12 لبناء أكواد المصدر.
  • MXNet 1.3.1 (أحدث إصدار ثابت في وقت كتابة هذا التقرير) ، تم تجميعه لبرنامج Scala 2.12.
  • الخادم مع بطاقات الرسومات نفيديا.
  • تثبيت Cuda 9.0 و Cudnn 7 على الخادم.
  • جافا 8 لتشغيل التعليمات البرمجية المترجمة.
  • عامل الميناء لسهولة التجميع والتسليم وإطلاق microservice على الخادم.

3. إعداد حاوية عامل ميناء الأساسية


بالنسبة إلى خدماتنا المصغرة ، ستحتاج إلى صورة Docker أساسية يتم فيها تثبيت الحد الأدنى لعدد التبعيات المطلوبة للتشغيل. للتجميع ، سوف نستخدم الصورة مع تثبيت إضافي. نعم ، سنقوم ببناء المصادر نفسها ليس في البيئة المحلية ، ولكن في حاوية Docker. سيؤدي ذلك إلى تسهيل الانتقال إلى التجميع عبر CI ، على سبيل المثال ، من خلال gitlab CI.

هيكل المجلد:

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


سيتم استخدام هذه الصورة للإطلاق النهائي للجهاز microservice. يعتمد هذا على الصورة الرسمية من Nvidia مع CUDA 9.0 المثبتة مسبقًا و CUDNN 7. تدعي وثائق MXNet 1.3.1 أنها تعمل مع CUDA 8.0 ، ولكن كما أظهرت الممارسة ، كل شيء يعمل بشكل جيد مع الإصدار 9.0 ، وحتى أسرع قليلاً.

بالإضافة إلى ذلك ، سنقوم بتثبيت Java 8 و MXNet 1.3.1 (سننشئها في إطار Scala 2.12) و OpenCV 3.4.3 وأداة Linux لتعيين المنطقة الزمنية في هذه الصورة.

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

البرامج النصية timeZone.sh java8.sh و opencv.sh تافهة للغاية ، لذلك لن أتناولها بالتفصيل ، فهي معروضة أدناه.

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} 

تثبيت MXNet ليس بهذه البساطة. الحقيقة هي أن جميع مجموعات هذه المكتبة لـ Scala مصنوعة على أساس إصدار المترجم 2.11 ، وهذا له ما يبرره ، لأن المكتبة تتضمن وحدة نمطية للعمل مع Spark ، والتي ، بدورها ، مكتوبة في Scala 2.11. بالنظر إلى أننا نستخدم Scala 2.12.7 في التطوير ، فإن المكتبات المترجمة ليست مناسبة لنا ، ولا يمكننا الانتقال إلى الإصدار 2.11. * لا يمكننا ، نظرًا للكم الكبير من الأكواد البرمجية المكتوبة بالفعل على الإصدار الجديد من Scala. ما يجب القيام به احصل على الكثير من المرح في جمع MXNet من المصدر لإصدارنا من Scala. أدناه ، سأقدم برنامج نصي لبناء وتثبيت MXNet 1.3.1 لـ Scala 2.12. * والتعليق على النقاط الرئيسية.

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 

الجزء الأكثر إثارة للاهتمام يبدأ بهذا السطر:

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

إذا قمت بتشغيل مجموعة MXNet كما هو موضح في التعليمات ، فسنحصل على خطأ. لا يمكن للمترجم العثور على مكتبة libcuda.so.1 ، لذلك سنربط من مكتبة libcuda.so إلى libcuda.so.1. قد لا يزعجك ذلك ، عندما تبدأ تشغيله على خادم إنتاج ، سنستبدل هذه المكتبة بمكتبة محلية. لاحظ أيضًا أنه تمت إضافة المسار إلى مكتبات CUDA من متغير البيئة LD_LIBRARY_PATH إلى LD_LIBRARY_PATH . إذا لم يتم ذلك ، فسيفشل التجميع أيضًا.

في هذه السطور ، استبدلنا إصدار Scala 2.11 بـ 2.12 في جميع الملفات الضرورية باستخدام تعبير عادي ، والذي تم اختياره بشكل تجريبي ، لأنه لا يكفي مجرد استبدال 2.11 في كل مكان بـ 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} && \ 

ثم يتم التعليق على الاعتماد على الوحدة النمطية للعمل مع Spark. إذا لم يتم ذلك ، فلن يتم تجميع المكتبة.

بعد ذلك ، قم بتشغيل التجميع ، كما هو موضح في التعليمات ، وانسخ المكتبة المجمعة إلى مجلد مشترك وقم بإزالة أي من البيانات المهملة التي قام Maven بضخها أثناء عملية الإنشاء (إذا لم يتم ذلك ، فستزيد الصورة النهائية بحوالي 3-4 جيجابايت ، مما قد يتسبب في DevOps الخاص بك ق العصبي).

نقوم بتجميع الصورة ، في الدليل الجذر للمشروع (انظر. هيكل المجلد):

 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 . 

واسمحوا لي أن أذكرك فقط في حالة أن النقطة في النهاية تقول إننا نقوم بالتجميع في سياق الدليل الحالي.

الآن حان الوقت للحديث عن صورة البناء.

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 

إنه أمر بسيط ، فنحن لا نحتاج إلى Scala و Sbt لبدء تشغيل خدماتنا المصغرة ، لذلك ليس هناك فائدة من جرهم إلى الصورة الأساسية للإطلاق. لذلك ، سنقوم بإنشاء صورة منفصلة سيتم استخدامها فقط للتجميع. البرامج النصية scala.sh و sbt.sh تافهة للغاية ولن أتناولها بالتفصيل.

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 

نقوم بتجميع الصورة ، في الدليل الجذر للمشروع (انظر. هيكل المجلد):

 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 . 

في نهاية المقال ، توجد روابط إلى المستودع تحتوي على كل هذه الملفات.

4. هيكل المشروع


بعد الانتهاء من التحضير لتجميع المشروع ، دعونا نفعل ما قررت قضاء بعض الوقت عليه في هذا المقال.

سيكون لمشروع الخدمة الميكروية لدينا الهيكل التالي:

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

هذا هو الهيكل القياسي لمشروع Scala ، باستثناء تبعيات الدلائل والنماذج.
يحتوي دليل التبعيات على مكتبة MXNet لـ Scala. يمكن الحصول عليها بطريقتين:

  • قم بإنشاء MXNet على الجهاز الذي ستقوم بتطويره (لاحظ أن المكتبة ليست مشتركة بين الأنظمة الأساسية ؛ وإذا كنت تقوم بإنشائها على Linux ، فلن تعمل على نظام Mac OS) ،
  • أو اسحبه خارج صورة Docker التي بنيناها سابقًا. إذا قررت إنشاء MXNet في بيئة محلية ، فسيساعدك البرنامج النصي mxnet_2.12.sh.

يمكنك سحب المكتبات من صورة Docker مثل هذا:

 #   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 

دليل النماذج يحتوي على ملفات شبكة عصبية مدربة ، يمكنك تنزيلها بحرية كما يلي:

 #   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 

لفترة وجيزة حول الملفات التي ليست ذات أهمية خاصة ، ولكن تلعب دورا في المشروع.

مشروع / بناء


 #   Sbt,   sbt.version = 1.2.6 

مشروع / plugins.sbt


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

src / main / resources / cat_and_dog.jpg


هذه صورة رائعة ، حيث تبحث شبكتنا العصبية عن قطة وكلب.


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


يخزن هذا الكائن المتغيرات العامة التي تتم قراءتها من متغيرات البيئة أو يتم تعيينها افتراضيًا.

 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


الكائن Run هو نقطة الدخول إلى التطبيق.

 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. تحميل الشبكة العصبية


يتم تحميل الشبكة العصبية في مُنشئ فئة 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) } 

في القسم ستقول أنه في الشبكة العصبية ستعمل مع NDArray بأبعاد 1 × 3 × 512 × 512 ، حيث 1 هي عدد الصور التي سيتم تضمينها في NDArray ، 3 هو عدد الألوان ، و 512 × 512 - حجم الصورة (يتم تعيين قيمة imageEdge = 12 في كائن simple.predict.Config ، وهذا هو حجم الجانب من الصورة المستخدمة لتدريب الشبكة العصبية). يتم تمرير كل وصف البيانات هذا إلى ObjectDetector .

هناك قسم آخر مثير للاهتمام وهو .

بعد تشغيل الصورة عبر الشبكة العصبية ، تكون النتيجة من النوع Seq[Seq[(String, Array[Float])]] . تحتوي المجموعة الأولى على نتيجة واحدة فقط (يتم تحديد تنسيق البيانات بواسطة شبكة عصبية محددة) ، ثم يكون كل عنصر من عناصر المجموعة التالية عبارة عن مجموعة مكونة من عنصرين:

  1. اسم الفئة ("قطة" ، "كلب" ، ...) ،
  2. صفيف من خمسة أرقام نقطة عائمة: الأول هو الاحتمال ، والثاني هو معامل حساب الإحداثي x ، والثالث هو معامل حساب الإحداثي y ، والرابع هو معامل حساب عرض المستطيل ، والخامس هو معامل حساب ارتفاع المستطيل.

للحصول على إحداثيات وأحجام المستطيل الفعلية ، تحتاج إلى ضرب العرض الأصلي وارتفاع الصورة بواسطة المعاملات المقابلة.
أسمح لنفسي باستطراد قليل حول موضوع NDArray . هذا صفيف متعدد الأبعاد تنشئه MXNet في سياق معين (CPU أو GPU). عند إنشاء NDArray ، يتم تكوين كائن C ++ ، وهو كائن يتم تنفيذ العمليات به بسرعة كبيرة (وإذا تم إنشاؤه في سياق وحدة معالجة الرسومات ، فإنه يكون فوريًا تقريبًا) ، ولكن عليك أن تدفع مقابل هذه السرعة. نتيجة لذلك (على الأقل في الإصدار MXNet 1.3.1) ، تحتاج إلى إدارة الذاكرة المخصصة لـ NDArray ، ولا تنسَ إلغاء تحميل هذه الكائنات من الذاكرة بعد الانتهاء من العمل معها. خلاف ذلك ، سيكون هناك تسرب ذاكرة كبير وسريع إلى حد ما ، وهو ليس مناسبًا جدًا للمراقبة ، لأن برامج ملفات تعريف JVM لا ترى ذلك. تتفاقم مشكلة الذاكرة إذا كنت تعمل في سياق GPU ، لأن بطاقات الفيديو لا تحتوي على قدر كبير من الذاكرة ويتعطل التطبيق بسرعة من الذاكرة.

كيفية حل مشكلة تسرب الذاكرة؟

في المثال أعلاه ، في السطر model.imageObjectDetect(image).head map toPrediction(image.getWidth, image.getHeight) filter (_.probability > threshold) ، يتم استخدام طريقة imageObjectDetect لتشغيل الصورة من خلال شبكة عصبية ، والتي تتلقى مدخلات BufferedImage . تتم جميع التحويلات من وإلى NDArray داخل الطريقة ، ولا تحتاج إلى التفكير في مشاكل إلغاء تخصيص الذاكرة. من ناحية أخرى ، قبل تحويل BufferedImage إلى NDArray يتم إجراء NDArray الحجم بحجم 512 × 512 وتتم تسوية الصورة باستخدام طرق كائن من النوع BufferedImage . يحدث هذا لفترة أطول قليلاً من عند استخدام OpenCV ، على سبيل المثال ، لكنه يحل مشكلة تحرير الذاكرة بعد استخدام NDArray .

يمكنك بالطبع استخدام OpenCV والتحكم في الذاكرة بنفسك ، لذلك تحتاج فقط إلى استدعاء طريقة NDArray من dispose ، لكن لسبب ما نسيت أن تذكر ذلك في وثائق MXNet الرسمية لـ Scala.

لدى MXNet أيضًا طريقة غير ملائمة للتحكم في تسرب الذاكرة الذي يحدث بسبب NDArray . للقيام بذلك ، قم بتشغيل التطبيق مع المعلمة JVM Dmxnet.traceLeakedObjects=true . إذا لاحظت NDArray ليست قيد الاستخدام ولكنها معلقة في الذاكرة ، فستحصل على استثناء يشير إلى أي سطر من التعليمات البرمجية NDArray .

نصيحتي: العمل مباشرة مع NDArray ، ومراقبة الذاكرة بعناية وكتابة التطبيع بنفسك ، بعد أن حددت مسبقًا الخوارزمية التي قام بها مهندس ML عند تدريب شبكة عصبية ، وإلا ستكون النتائج مختلفة تمامًا. يحتوي ObjectDetector على طريقة objectDetectWithNDArray والتي يمكنك من خلالها تمرير NDArray . لتطبيق نهج أكثر عالمية لتحميل شبكة عصبية ، أوصي باستخدام كائن org.apache.mxnet.module.Module . أدناه مثال على الاستخدام.

 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. REST تنفيذ API


فئة simple.predictor.Server هي المسؤولة عن تطبيق واجهة برمجة تطبيقات REST. يعتمد الخادم على خادم Java المضمن في 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. اختبار


للتحقق ، ابدأ تشغيل الخادم وأرسل صورة اختبار src / main / resources / cat_and_dog.jpg. سنقوم بتحليل JSON المستلم من الخادم ، ونحقق من عدد الكائنات العصبية الموجودة في الصورة وما هي الأشياء الموجودة فيها ، ودائرة الكائنات الموجودة في الصورة.

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

الخاتمة


MXNet , - . , , , production.

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

, .

, . . شكرا لاهتمامكم

المراجع


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


All Articles