يوجد على شبكة الإنترنت عدد كبير من الأدلة والأمثلة ، والتي على أساسها ستتمكن ، أيها القراء الأعزاء ، من كتابة الكود "دون صعوبة كبيرة" وفي الوقت "الأدنى" الذي يميز القطط عن الكلاب في الصورة. ولماذا نضيع الوقت في هذا المقال؟
الرئيسية ، في رأيي ، العيب في كل هذه الأمثلة هو الاحتمالات المحدودة. لقد أخذت مثالاً - حتى مع الشبكة العصبية الأساسية التي يقدمها المؤلف - أطلقتها ، وربما نجحت ، وماذا بعد؟ كيفية جعل هذا الكود البسيط يبدأ العمل على خادم الإنتاج؟ كيفية تحديثها وصيانتها؟ هذا هو المكان الذي تبدأ فيه المتعة. لم أتمكن من العثور على وصف كامل للعملية منذ اللحظة "حسنًا ، قام مهندس ML بتدريب الشبكة العصبية" على "أخيرًا تم طرحها في الإنتاج". وقررت إغلاق هذه الفجوة.
لن أتحدث عن كيفية تعليم الشبكة العصبية أشياء مضحكة جديدة ترضيك وتساعدك على كسب مجموعة من الأوراق النقدية المقرمشة. هذا موضوع رائع لمقال منفصل. على سبيل المثال ، سأستخدم شبكة عصبية يمكن تنزيلها مجانًا. المهمة الرئيسية التي حددتها لنفسي هي تقديم وصف كامل لعملية إدخال شبكة عصبية في العملية.
أجب على الفور على السؤال "لماذا لا يوجد في Python؟": نحن نستخدم Scala لحلول الإنتاج بسبب الكتابة الأكثر ملاءمة وثباتًا للكود متعدد الخيوط.
المحتويات
1. بيان المشكلة2. التقنيات المستخدمة3. إعداد حاوية عامل ميناء الأساسية4. هيكل المشروع5. تحميل الشبكة العصبية6. REST تنفيذ API7. اختبار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
java8.sh
opencv.sh
تثبيت 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
الجزء الأكثر إثارة للاهتمام يبدأ بهذا السطر:
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
sbt.sh
نقوم بتجميع الصورة ، في الدليل الجذر للمشروع (انظر. هيكل المجلد):
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 مثل هذا:
دليل النماذج يحتوي على ملفات شبكة عصبية مدربة ، يمكنك تنزيلها بحرية كما يلي:
لفترة وجيزة حول الملفات التي ليست ذات أهمية خاصة ، ولكن تلعب دورا في المشروع.
مشروع / بناء
# Sbt, sbt.version = 1.2.6
مشروع / plugins.sbt
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"
simple.predictor.Config
يخزن هذا الكائن المتغيرات العامة التي تتم قراءتها من متغيرات البيئة أو يتم تعيينها افتراضيًا.
package simple.predictor import org.apache.mxnet.Context import scala.util.Try object Config {
simple.predictor.Run
الكائن Run هو نقطة الدخول إلى التطبيق.
package simple.predictor import java.net.InetSocketAddress
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) {
في القسم
ستقول أنه في الشبكة العصبية ستعمل مع
NDArray
بأبعاد 1 × 3 × 512 × 512 ، حيث 1 هي عدد الصور التي سيتم تضمينها في NDArray ، 3 هو عدد الألوان ، و 512 × 512 - حجم الصورة (يتم تعيين قيمة
imageEdge = 12
في كائن
simple.predict.Config
، وهذا هو حجم الجانب من الصورة المستخدمة لتدريب الشبكة العصبية). يتم تمرير كل وصف البيانات هذا إلى
ObjectDetector
.
هناك قسم آخر مثير للاهتمام وهو
.
بعد تشغيل الصورة عبر الشبكة العصبية ، تكون النتيجة من النوع
Seq[Seq[(String, Array[Float])]]
. تحتوي المجموعة الأولى على نتيجة واحدة فقط (يتم تحديد تنسيق البيانات بواسطة شبكة عصبية محددة) ، ثم يكون كل عنصر من عناصر المجموعة التالية عبارة عن مجموعة مكونة من عنصرين:
- اسم الفئة ("قطة" ، "كلب" ، ...) ،
- صفيف من خمسة أرقام نقطة عائمة: الأول هو الاحتمال ، والثاني هو معامل حساب الإحداثي
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
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) {
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 {
, .

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
( , , , ).
:
الخاتمة
MXNet , - . , , , production.
, , MXNet , Python production Scala, Java ++.
, .
, . . شكرا لاهتمامكم
المراجع