Scala + MXNet = Microsserviço com neurônio em prod


Há um grande número de manuais e exemplos na Internet, com base nos quais vocês, queridos leitores, poderão “sem muita dificuldade” e com custos de tempo “mínimos” escrever código que pode distinguir gatos de cães em uma foto. E por que perder tempo com este artigo?

A principal, na minha opinião, a desvantagem de todos esses exemplos são as possibilidades limitadas. Você pegou um exemplo - mesmo com a rede neural básica que o autor oferece - lançou, talvez até funcionou, e o que vem depois? Como fazer esse código simples começar a funcionar em um servidor de produção? Como atualizá-lo e mantê-lo? É aqui que a diversão começa. Não consegui encontrar uma descrição completa do processo a partir do momento "Bem, o engenheiro de ML treinou a rede neural" para "finalmente a lançamos em produção". E eu decidi fechar essa lacuna.

Não falarei sobre como ensinar à rede neural novas coisas engraçadas que irão agradá-lo e ajudá-lo a ganhar um monte de notas crocantes. Este é um ótimo tópico para um artigo separado. Como exemplo, usarei uma rede neural que pode ser baixada gratuitamente. A principal tarefa que me propus é fornecer uma descrição completa do processo de introdução de uma rede neural em operação.

Respondo imediatamente à pergunta “Por que não no Python?”: Usamos o Scala para soluções de produção devido à escrita mais conveniente e estável do código multithread.

Conteúdo


1. Declaração do problema
2. Tecnologias utilizadas
3. Preparando um Contêiner de Docker Básico
4. Estrutura do projeto
5. Carregamento de rede neural
6. Implementação da API REST
7. Teste
8. Montagem de um microsserviço com base em uma imagem básica
9. Iniciando um microsserviço em um servidor de produção com uma GPU
Conclusão
Referências

1. Declaração do problema


Suponha que tenhamos um grande banco de dados de fotos com objetos diferentes e precisamos criar um microsserviço que receba uma imagem em uma solicitação HTTP POST e responda no formato JSON. A resposta deve conter o número de objetos encontrados e suas classes, o grau de probabilidade de que esse seja exatamente o objeto da classe declarada e as coordenadas dos retângulos que cobrem os limites de cada objeto.

2. Tecnologias utilizadas


  • Scala 2.12.7 + conjunto mínimo de bibliotecas adicionais, Sbt 1.2.6 com o plug-in Sbt-pack 0.12 para criar códigos-fonte.
  • MXNet 1.3.1 (a última versão estável no momento da redação), compilada para o Scala 2.12.
  • Servidor com placas gráficas Nvidia.
  • Cuda 9.0 e Cudnn 7 instalados no servidor.
  • Java 8 para executar o código compilado.
  • Docker para facilitar a montagem, entrega e lançamento do microsserviço no servidor.

3. Preparando um Contêiner de Docker Básico


Para o nosso microsserviço, você precisará de uma imagem básica do Docker, na qual o número mínimo de dependências necessárias para executar será instalado. Para montagem, usaremos a imagem com o Sbt instalado adicionalmente. Sim, criaremos as próprias fontes não no ambiente local, mas no contêiner do Docker. Isso facilitará a transição adicional para a montagem através do IC, por exemplo, através do CI do gitlab.

Estrutura de pastas:

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


Esta imagem será usada para o lançamento final do microsserviço. Ele é baseado na imagem oficial da Nvidia com CUDA 9.0 e CUDNN 7. pré-instalados. A documentação do MXNet 1.3.1 afirma funcionar com o CUDA 8.0, mas, como a prática demonstrou, tudo funciona bem com a versão 9.0 e até um pouco mais rápido.

Além disso, instalaremos o Java 8, o MXNet 1.3.1 (o construiremos sob o Scala 2.12), o OpenCV 3.4.3 e o utilitário Linux para definir o fuso horário nesta imagem.

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

Os scripts timeZone.sh java8.sh e opencv.sh são bastante triviais, por isso não vou me debruçar sobre eles em detalhes, eles são apresentados abaixo.

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} 

Instalar o MXNet não é tão simples. O fato é que todos os assemblies desta biblioteca para o Scala são feitos com base na versão 2.11 do compilador, e isso é justificado, pois a biblioteca inclui um módulo para trabalhar com o Spark, que, por sua vez, está escrito no Scala 2.11. Considerando que usamos o Scala 2.12.7 no desenvolvimento, as bibliotecas compiladas não são adequadas para nós e não podemos ir para a versão 2.11. * Não podemos, devido à grande quantidade de código já escrita na nova versão do Scala. O que fazer Divirta-se muito coletando o MXNet da fonte para a nossa versão do Scala. Abaixo darei um script para construir e instalar o MXNet 1.3.1 para Scala 2.12. * E comente os pontos principais.

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 

A parte mais interessante começa com esta linha:

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

Se você executar o conjunto MXNet como nas instruções, obteremos um erro. O compilador não pode encontrar a biblioteca libcuda.so.1, portanto, vincularemos a partir da biblioteca libcuda.so ao libcuda.so.1. Isso pode não incomodá-lo. Quando você o inicia em um servidor de produção, substituiremos esta biblioteca por uma local. Observe também que o caminho para as bibliotecas CUDA da variável de ambiente CUDA_STUBS_DIR foi adicionado ao LD_LIBRARY_PATH . Se isso não for feito, a montagem também falhará.

Nestas linhas, substituímos a versão do Scala 2.11 pela 2.12 em todos os arquivos necessários, usando uma expressão regular, que foi selecionada experimentalmente, porque não é suficiente substituir simplesmente o 2.11 em todos os lugares pelo 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} && \ 

E então a dependência do módulo para trabalhar com o Spark é comentada. Se isso não for feito, a biblioteca não será montada.

Em seguida, execute o assembly, conforme indicado nas instruções, copie a biblioteca montada em uma pasta compartilhada e remova todo o lixo que o Maven lançou durante o processo de compilação (se isso não for feito, a imagem final aumentará em cerca de 3-4 GB, o que pode causar os DevOps está nervoso).

Coletamos a imagem, estando no diretório raiz do projeto (consulte. Estrutura de pastas):

 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 . 

Deixe-me lembrá-lo para o caso de o ponto no final indicar que estamos fazendo a montagem no contexto do diretório atual.

Agora é hora de falar sobre a imagem de compilação.

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 

É simples, não precisamos do Scala e do Sbt para iniciar nosso microsserviço, portanto não faz sentido arrastá-los para a imagem base para o lançamento. Portanto, criaremos uma imagem separada que será usada apenas para montagem. Os scripts scala.sh e sbt.sh são bastante triviais e não vou me aprofundar neles em detalhes.

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 

Coletamos a imagem, estando no diretório raiz do projeto (consulte. Estrutura de pastas):

 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 . 

No final do artigo, existem links para o repositório com todos esses arquivos.

4. Estrutura do projeto


Depois de terminar de preparar a montagem do projeto, vamos fazer o que você decidiu dedicar tempo para este artigo.

O projeto do nosso microsserviço terá a seguinte estrutura:

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

Essa é a estrutura padrão de um projeto Scala, com exceção das dependências e modelos de diretórios.
O diretório de dependências contém a biblioteca MXNet para Scala. Pode ser obtido de duas maneiras:

  • construa o MXNet na máquina em que você estará desenvolvendo (observe que a biblioteca não é multiplataforma; se você construí-la no Linux, ela não funcionará no Mac OS),
  • ou retire-o da imagem do Docker que criamos anteriormente. Se você decidir construir o MXNet em um ambiente local, o script mxnet_2.12.sh o ajudará.

Você pode extrair bibliotecas da imagem do Docker assim:

 #   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 

O diretório de modelos contém os arquivos de uma rede neural treinada, você pode baixá-los livremente da seguinte maneira:

 #   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 

Brevemente sobre arquivos que não são de interesse particular, mas que desempenham um papel no projeto.

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


Uma imagem tão maravilhosa, em que nossa rede neural procurará um gato e um cachorro.


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


Este objeto armazena variáveis ​​globais cujo valor é lido das variáveis ​​de ambiente ou definido por padrão.

 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


O objeto Executar é o ponto de entrada para o aplicativo.

 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. Carregamento de rede neural


A rede neural é carregada no construtor da classe 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) } 

Na seção você diz que em uma rede neural funcionará com o NDArray com uma dimensão de 1 x 3 x 512 x 512, em que 1 é o número de imagens que estarão contidas no NDArray, 3 é o número de cores e 512 x 512 - tamanho da imagem (o valor de imageEdge = 12 é definido no objeto simple.predict.Config , esse é o tamanho lateral da imagem usada para treinar a rede neural). Toda essa descrição de dados é passada para o ObjectDetector .

Outra seção interessante é o .

Depois de executar a imagem pela rede neural, o resultado é do tipo Seq[Seq[(String, Array[Float])]] . A primeira coleção contém apenas um resultado (o formato dos dados é determinado por uma rede neural específica), então cada elemento da próxima coleção é uma tupla de dois elementos:

  1. nome da classe ("gato", "cachorro", ...),
  2. uma matriz de cinco números de ponto flutuante: o primeiro é a probabilidade, o segundo é o coeficiente para calcular a coordenada x , o terceiro é o coeficiente para calcular a coordenada y , o quarto é o coeficiente para calcular a largura do retângulo e o quinto é o coeficiente para calcular a altura do retângulo.

Para obter os valores reais das coordenadas e dimensões do retângulo, é necessário multiplicar a largura e a altura originais da imagem pelos coeficientes correspondentes.
NDArray me um pouco de digressão sobre o tópico NDArray . Essa é uma matriz multidimensional que o MXNet cria em um determinado contexto (CPU ou GPU). Ao criar um NDArray, um objeto C ++ é formado, um objeto com o qual as operações são executadas muito rapidamente (e se ele é criado em um contexto de GPU, é quase instantâneo), mas você precisa pagar por essa velocidade. Como resultado (pelo menos na versão MXNet 1.3.1), você precisa gerenciar independentemente a memória alocada para o NDArray e não se esqueça de descarregar esses objetos da memória após concluir o trabalho com eles. Caso contrário, haverá um vazamento de memória significativo e bastante rápido, o que não é muito conveniente para monitorar, pois os programas para criação de perfil da JVM não o veem. O problema de memória é agravado se você trabalhar em um contexto de GPU, pois as placas de vídeo não possuem uma grande quantidade de memória e o aplicativo falha rapidamente na memória.

Como resolver um problema de vazamento de memória?

No exemplo acima, no modelo de linha model.imageObjectDetect(image).head map toPrediction(image.getWidth, image.getHeight) filter (_.probability > threshold) , o método imageObjectDetect é usado para executar a imagem pela rede neural, que recebe uma entrada BufferedImage . Todas as conversões para e do NDArray são feitas dentro do método, e você não precisa pensar em problemas de desalocação de memória. Por outro lado, antes de converter BufferedImage em NDArray é realizado no tamanho de 512 x 512 e a imagem é normalizada usando métodos de um objeto do tipo BufferedImage . Isso acontece um pouco mais do que quando se usa o OpenCV, por exemplo, mas resolve o problema de liberar memória após o uso do NDArray .

Obviamente, você pode usar o OpenCV e controlar a memória por conta própria. Para isso, basta chamar o método de NDArray do dispose , mas, por algum motivo, esqueceu de mencionar isso na documentação oficial do MXNet para Scala.

O MXNet também possui uma maneira não muito conveniente de controlar o vazamento de memória que ocorre devido ao NDArray . Para fazer isso, execute o aplicativo com o parâmetro JVM Dmxnet.traceLeakedObjects=true . Se o MXNet perceber um NDArray que não está em uso, mas está NDArray na memória, você receberá uma exceção indicando em qual linha de código o infeliz NDArray .

Meu conselho: trabalhe diretamente com o NDArray, monitore cuidadosamente a memória e escreva a normalização, especificando previamente o algoritmo que o engenheiro de ML fez ao treinar uma rede neural, caso contrário, os resultados serão completamente diferentes. O ObjectDetector possui um método objectDetectWithNDArray para o qual você pode transmitir um NDArray . Para implementar uma abordagem mais universal para carregar uma rede neural, recomendo usar o objeto org.apache.mxnet.module.Module . Abaixo está um exemplo de uso.

 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. Implementação da API REST


A classe simple.predictor.Server é responsável pela implementação da API REST. O servidor é baseado no servidor Java incluído no 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. Teste


Para verificar, inicie o servidor e envie uma imagem de teste src / main / resources / cat_and_dog.jpg. Analisaremos o JSON recebido do servidor, verificaremos quantos e quais objetos a rede neural encontrou na imagem e circularemos os objetos na imagem.

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

Conclusão


MXNet , - . , , , production.

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

, .

, . . Obrigado pela atenção.

Referências


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


All Articles