Ada sejumlah besar manual dan contoh di Internet, yang menjadi dasar Anda, para pembaca yang budiman, akan dapat “tanpa banyak kesulitan” dan dengan biaya waktu “minimal” menulis kode yang dapat membedakan kucing dari anjing dalam sebuah foto. Dan mengapa kemudian membuang waktu untuk artikel ini?
Menurut saya, yang utama dari semua contoh ini adalah kemungkinan yang terbatas. Anda mengambil contoh - bahkan dengan jaringan saraf dasar yang ditawarkan penulis - meluncurkannya, bahkan mungkin berhasil, dan apa selanjutnya? Bagaimana membuat kode sederhana ini mulai bekerja di server produksi? Bagaimana cara memperbarui dan memeliharanya? Di sinilah kesenangan dimulai. Saya tidak dapat menemukan deskripsi lengkap dari proses dari saat "Ya, insinyur ML melatih jaringan saraf" hingga "akhirnya kami meluncurkannya ke dalam produksi". Dan saya memutuskan untuk menutup celah ini.
Saya tidak akan berbicara tentang cara mengajar jaringan saraf hal-hal lucu baru yang akan menyenangkan Anda dan membantu Anda mendapatkan banyak uang kertas renyah. Ini adalah topik yang bagus untuk artikel terpisah. Sebagai contoh, saya akan menggunakan jaringan saraf yang dapat diunduh secara bebas. Tugas utama yang saya tetapkan sendiri adalah untuk memberikan deskripsi lengkap tentang proses memperkenalkan jaringan saraf ke dalam operasi.
Saya segera menjawab pertanyaan "Mengapa tidak dengan Python?": Kami menggunakan Scala untuk solusi produksi karena penulisan kode multi-utas yang lebih nyaman dan stabil.
Isi
1. Pernyataan masalah2. Teknologi yang digunakan3. Mempersiapkan wadah buruh pelabuhan dasar4. Struktur proyek5. Pemuatan jaringan saraf6. Implementasi REST API7. Pengujian8. Merakit layanan microser berdasarkan gambar dasar9. Memulai layanan microser pada server produksi dengan GPUKesimpulanReferensi1. Pernyataan masalah
Misalkan kita memiliki database besar foto dengan objek yang berbeda, dan kita perlu membuat layanan mikro yang akan menerima gambar dalam permintaan HTTP POST dan merespons dalam format JSON. Jawabannya harus berisi jumlah objek yang ditemukan dan kelas mereka, tingkat probabilitas bahwa ini persis objek kelas yang dideklarasikan, dan koordinat persegi panjang yang mencakup batas-batas setiap objek.
2. Teknologi yang digunakan
- Scala 2.12.7 + set minimum perpustakaan tambahan, Sbt 1.2.6 dengan plugin Sbt-pack 0.12 untuk membangun kode sumber.
- MXNet 1.3.1 (versi stabil terbaru pada saat penulisan), dikompilasi untuk Scala 2.12.
- Server dengan kartu grafis Nvidia.
- Cuda 9.0 dan Cudnn 7 diinstal pada server.
- Java 8 untuk menjalankan kode yang dikompilasi.
- Docker untuk kemudahan perakitan, pengiriman dan peluncuran microservice di server.
3. Mempersiapkan wadah buruh pelabuhan dasar
Untuk layanan Microsoft kami, Anda akan memerlukan gambar Docker dasar di mana jumlah minimum dependensi yang diperlukan untuk menjalankan akan diinstal. Untuk perakitan, kami akan menggunakan gambar dengan Sbt yang diinstal tambahan. Ya, kami akan membangun sumbernya sendiri bukan di lingkungan lokal, tetapi di wadah Docker. Ini akan memfasilitasi transisi lebih lanjut ke perakitan melalui CI, misalnya, melalui gitlab CI.
Struktur folder:
\ | ----- 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
Gambar ini akan digunakan untuk peluncuran microservice terakhir. Ini didasarkan pada gambar resmi dari Nvidia dengan CUDA 9.0 dan CUDNN yang telah diinstal 7. Dokumentasi untuk MXNet 1.3.1 mengklaim bekerja dengan CUDA 8.0, tetapi, seperti yang telah ditunjukkan, semuanya berjalan baik dengan versi 9.0, dan bahkan sedikit lebih cepat.
Selain itu, kami akan menginstal Java 8, MXNet 1.3.1 (kami akan membangunnya di bawah Scala 2.12), OpenCV 3.4.3, dan utilitas Linux untuk mengatur zona waktu pada gambar ini.
# 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 / /
Script timeZone.sh java8.sh dan opencv.sh cukup sepele, jadi saya tidak akan membahasnya secara rinci, mereka disajikan di bawah ini.
timeZone.sh
java8.sh
opencv.sh
Menginstal MXNet tidak sesederhana itu. Faktanya adalah bahwa semua rakitan perpustakaan ini untuk Scala dibuat berdasarkan versi kompilator 2.11, dan ini dibenarkan, karena perpustakaan menyertakan modul untuk bekerja dengan Spark, yang, pada gilirannya, ditulis dalam Scala 2.11. Menimbang bahwa kami menggunakan Scala 2.12.7 dalam pengembangan, perpustakaan yang dikompilasi tidak cocok untuk kami, dan kami tidak dapat turun ke versi 2.11. * Kami tidak bisa, karena banyaknya kode yang sudah ditulis pada Scala versi baru. Apa yang harus dilakukan Dapatkan banyak kesenangan mengumpulkan MXNet dari sumber untuk versi Scala kami. Di bawah ini saya akan memberikan skrip untuk membangun dan menginstal MXNet 1.3.1 untuk Scala 2.12. * Dan mengomentari poin utama.
mxnet_2_12.sh
Bagian yang paling menarik dimulai dengan baris ini:
ln -s ${CUDA_STUBS_DIR}/libcuda.so ${CUDA_STUBS_DIR}/libcuda.so.1 && \
Jika Anda menjalankan perakitan MXNet seperti dalam instruksi, kami akan mendapatkan kesalahan. Kompiler tidak dapat menemukan perpustakaan libcuda.so.1, jadi kami akan menautkan dari perpustakaan libcuda.so ke libcuda.so.1. Ini mungkin tidak mengganggu Anda, ketika Anda memulainya di server produksi, kami akan mengganti perpustakaan ini dengan yang lokal. Perhatikan juga bahwa jalur ke pustaka CUDA dari variabel lingkungan
CUDA_STUBS_DIR
telah ditambahkan ke
LD_LIBRARY_PATH
. Jika ini tidak dilakukan, maka perakitan juga akan gagal.
Pada baris ini, kami mengganti versi Scala 2.11 dengan 2.12 di semua file yang diperlukan menggunakan ekspresi reguler, yang dipilih secara eksperimental, karena tidak cukup hanya mengganti 2.11 di mana-mana dengan 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} && \
Dan kemudian ketergantungan pada modul untuk bekerja dengan Spark dikomentari. Jika ini tidak dilakukan, perpustakaan tidak akan dirakit.
Selanjutnya, jalankan perakitan, seperti yang ditunjukkan dalam instruksi, salin perpustakaan yang dirakit ke folder bersama dan hapus semua sampah yang dipompa Maven selama proses membangun (jika ini tidak dilakukan, gambar akhir akan tumbuh sekitar 3-4 GB, yang dapat menyebabkan DevOps Anda S gugup).
Kami mengumpulkan gambar, berada di direktori root proyek (lihat. Struktur folder):
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 .
Biarkan saya mengingatkan Anda untuk berjaga-jaga jika titik di bagian akhir mengatakan bahwa kami sedang melakukan perakitan dalam konteks direktori saat ini.
Sekarang saatnya berbicara tentang build image.
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
Sederhana saja, kita tidak perlu Scala dan Sbt untuk memulai layanan microser kami, jadi tidak ada gunanya menyeret mereka ke gambar dasar untuk diluncurkan. Oleh karena itu, kami akan membuat gambar terpisah yang hanya akan digunakan untuk perakitan. Skrip scala.sh dan sbt.sh cukup sepele dan saya tidak akan membahasnya secara rinci.
scala.sh
sbt.sh
Kami mengumpulkan gambar, berada di direktori root proyek (lihat. Struktur folder):
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 .
Di akhir artikel ada tautan ke repositori dengan semua file ini.
4. Struktur proyek
Setelah selesai mempersiapkan untuk perakitan proyek, mari kita lakukan apa yang Anda putuskan untuk menghabiskan waktu untuk artikel ini.
Proyek microservice kami akan memiliki struktur berikut:
\ | ----- 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
Ini adalah struktur standar proyek Scala, dengan pengecualian direktori dan dependensi direktori.
Direktori dependensi berisi perpustakaan MXNet untuk Scala. Itu dapat diperoleh dengan dua cara:
- membangun MXNet di mesin tempat Anda akan mengembangkan (perhatikan bahwa perpustakaan tidak lintas platform; jika Anda membangunnya di Linux, itu tidak akan berfungsi pada Mac OS),
- atau tarik keluar dari gambar Docker yang kami buat sebelumnya. Jika Anda memutuskan untuk membangun MXNet di lingkungan lokal, maka skrip mxnet_2.12.sh akan membantu Anda.
Anda dapat menarik pustaka dari gambar Docker seperti ini:
Direktori models berisi file-file dari jaringan saraf yang terlatih, Anda dapat mengunduhnya secara bebas sebagai berikut:
Selanjutnya secara singkat tentang file yang tidak menarik, tetapi berperan dalam proyek ini.
project / build.properties
# Sbt, sbt.version = 1.2.6
project / plugins.sbt
src / main / resources / cat_and_dog.jpg
Gambaran yang begitu indah, di mana jaringan saraf kita akan mencari kucing dan anjing.

build.sbt
enablePlugins(PackPlugin) name := "simple-predictor" version := "0.1" scalaVersion := "2.12.7" unmanagedBase := baseDirectory.value / "dependencies"
simple.predictor.Config
Objek ini menyimpan variabel global yang nilainya dibaca dari variabel lingkungan atau ditetapkan secara default.
package simple.predictor import org.apache.mxnet.Context import scala.util.Try object Config {
simple.predictor.Run
Objek Run adalah titik masuk ke aplikasi.
package simple.predictor import java.net.InetSocketAddress
5. Pemuatan jaringan saraf
Jaringan saraf dimuat dalam konstruktor kelas
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) {
Di bagian
Anda mengatakan bahwa dalam jaringan saraf itu akan bekerja dengan
NDArray
dengan dimensi 1 x 3 x 512 x 512, di mana 1 adalah jumlah gambar yang akan terkandung dalam NDArray, 3 adalah jumlah warna, dan 512 x 512 - ukuran gambar (nilai
imageEdge = 12
diatur dalam objek
simple.predict.Config
, ini adalah ukuran sisi gambar yang digunakan untuk melatih jaringan saraf). Semua deskripsi data ini diteruskan ke
ObjectDetector
.
Bagian lain yang menarik adalah
.
Setelah menjalankan gambar melalui jaringan saraf, hasilnya adalah dari tipe
Seq[Seq[(String, Array[Float])]]
. Koleksi pertama hanya berisi satu hasil (format data ditentukan oleh jaringan saraf tertentu), maka setiap elemen koleksi berikutnya adalah tupel dari dua elemen:
- nama kelas ("kucing", "anjing", ...),
- array dari lima angka floating point: yang pertama adalah probabilitas, yang kedua adalah koefisien untuk menghitung koordinat
x
, yang ketiga adalah koefisien untuk menghitung koordinat y
, yang keempat adalah koefisien untuk menghitung lebar persegi panjang, dan yang kelima adalah koefisien untuk menghitung ketinggian persegi panjang.
Untuk mendapatkan koordinat aktual dan ukuran persegi panjang, Anda perlu mengalikan lebar asli dan tinggi gambar dengan koefisien yang sesuai.
Saya membiarkan diri saya sedikit menyimpang tentang topik
NDArray
. Ini adalah array multidimensi yang dibuat MXNet dalam konteks tertentu (CPU atau GPU). Saat membuat NDArray, objek C ++ terbentuk, objek yang dengannya operasi dilakukan dengan sangat cepat (dan jika dibuat dalam konteks GPU, itu hampir seketika), tetapi Anda harus membayar untuk kecepatan tersebut. Akibatnya (setidaknya dalam versi MXNet 1.3.1) Anda perlu mengelola memori yang dialokasikan secara
NDArray
untuk
NDArray
, dan jangan lupa untuk membongkar objek-objek ini dari memori setelah Anda selesai bekerja dengannya. Jika tidak, akan ada kebocoran memori yang signifikan dan cukup cepat, yang sangat tidak nyaman untuk dipantau, karena program untuk profil JVM tidak melihatnya. Masalah memori diperburuk jika Anda bekerja dalam konteks GPU, karena kartu video tidak memiliki banyak memori dan aplikasi macet dengan cepat kehabisan memori.
Bagaimana mengatasi masalah kebocoran memori?
Dalam contoh di atas, pada model
model.imageObjectDetect(image).head map toPrediction(image.getWidth, image.getHeight) filter (_.probability > threshold)
, metode
imageObjectDetect
digunakan untuk menjalankan gambar melalui jaringan saraf, yang menerima input
BufferedImage
. Semua konversi ke dan dari
NDArray
dilakukan di dalam metode, dan Anda tidak perlu memikirkan masalah alokasi memori. Di sisi lain, sebelum mengubah
BufferedImage
ke
NDArray
dilakukan pada ukuran 512 x 512 dan gambar dinormalisasi menggunakan metode objek objek tipe
BufferedImage
. Ini terjadi sedikit lebih lama daripada ketika menggunakan OpenCV, misalnya, tetapi ini memecahkan masalah membebaskan memori setelah menggunakan
NDArray
.
Anda dapat, tentu saja, menggunakan OpenCV dan mengontrol memori sendiri, untuk ini Anda hanya perlu memanggil metode
NDArray
dispose
, tetapi untuk beberapa alasan Anda lupa menyebutkan ini dalam dokumentasi MXNet resmi untuk Scala.
MXNet juga memiliki cara yang tidak terlalu nyaman untuk mengontrol kebocoran memori yang terjadi karena
NDArray
. Untuk melakukan ini, jalankan aplikasi dengan parameter JVM
Dmxnet.traceLeakedObjects=true
. Jika MXNet memperhatikan
NDArray
yang tidak digunakan tetapi tergantung pada memori, Anda akan mendapatkan pengecualian yang menunjukkan baris kode mana
NDArray
naas
NDArray
.
Saran saya: bekerja langsung dengan NDArray, hati-hati memonitor memori dan menulis normalisasi sendiri, setelah sebelumnya menentukan algoritma apa yang dilakukan insinyur ML ketika melatih jaringan saraf, jika tidak hasilnya akan sangat berbeda.
ObjectDetector
memiliki metode
objectDetectWithNDArray
mana Anda dapat melewati
NDArray
. Untuk menerapkan pendekatan yang lebih universal untuk memuat jaringan saraf, saya sarankan menggunakan objek
org.apache.mxnet.module.Module
. Di bawah ini adalah contoh penggunaan.
import org.apache.mxnet._ import org.apache.mxnet.io.NDArrayIter
6. Implementasi REST API
Kelas
simple.predictor.Server
bertanggung jawab untuk mengimplementasikan REST API. Server didasarkan pada server Java yang termasuk dalam 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. Pengujian
Untuk memeriksa, mulai server dan mengirim gambar uji src / main / resources / cat_and_dog.jpg. Kami akan mengurai JSON yang diterima dari server, memeriksa berapa banyak dan objek apa yang ditemukan jaringan saraf dalam gambar, dan melingkari objek dalam gambar.
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
( , , , ).
:
Kesimpulan
MXNet , - . , , , production.
, , MXNet , Python production Scala, Java ++.
, .
, . . Terima kasih atas perhatian anda
Referensi