互联网上有大量的手册和示例,亲爱的读者们,在这些手册和示例的基础上,您将能够“毫无困难”地,以“最少的”时间成本编写代码,以区分照片中的猫和狗。 那为什么还要在本文上浪费时间呢?
我认为所有这些示例的主要缺点是可能性有限。 您举了一个例子-甚至使用了作者提供的基本神经网络-都启动了它,也许它甚至起作用了,接下来该怎么办? 如何使这个简单的代码开始在生产服务器上工作? 如何更新和维护它? 这就是乐趣的开始。 从“好吧,ML工程师训练了神经网络”到“最终我们将其推广到生产中”那一刻,我找不到该过程的完整描述。 我决定缩小这一差距。
我不会谈论如何教神经网络一些有趣的新事物,这些事物会取悦您并帮助您赚取一堆香脆的钞票。 这是单独撰写文章的重要话题。 例如,我将使用可以免费下载的神经网络。 我设定的主要任务是全面介绍将神经网络引入运营的过程。
我立即回答“为什么不使用Python?”这个问题:我们将Scala用于生产解决方案,因为它更加方便,稳定地编写了多线程代码。
目录内容
1.问题陈述2.使用的技术3.准备一个基本的Docker容器4.项目结构5.神经网络加载6. REST API实施7.测试8.基于基本映像组装微服务9.在带有GPU的生产服务器上启动微服务结论参考文献1.问题陈述
假设我们有一个包含不同对象的大型照片数据库,我们需要制作一个微服务,该服务将以HTTP POST请求接收图片并以JSON格式响应。 答案应包含找到的对象及其类的数量,该对象恰好是所声明类的对象的概率以及覆盖每个对象边界的矩形的坐标。
2.使用的技术
- Scala 2.12.7 +最少的附加库集,带有Sbt-pack 0.12插件的Sbt 1.2.6,用于构建源代码。
- MXNet 1.3.1(在撰写本文时为最新的稳定版本),针对Scala 2.12进行了编译。
- 装有Nvidia显卡的服务器。
- 服务器上安装了Cuda 9.0和Cudnn 7。
- Java 8运行已编译的代码。
- Docker简化了服务器上微服务的组装,交付和启动。
3.准备一个基本的Docker容器
对于我们的微服务,您将需要一个基本的Docker映像,其中将安装运行所需的最小数量的依赖项。 对于组装,我们将使用带有附加安装的Sbt的映像。 是的,我们不会在本地环境中而是在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-运行时
该图像将用于微服务的最终启动。 它基于Nvidia的官方映像,其中预装了CUDA 9.0和CUDNN7。MXNet1.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已经编写了大量代码,因此我们不能。 怎么办 从我们的Scala版本中获得从源代码收集MXNet的乐趣。 下面,我将给出一个脚本,用于为Scala 2.12构建和安装MXNet 1.3.1。*并对要点进行评论。
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_STUBS_DIR
环境变量到CUDA库的路径已添加到
LD_LIBRARY_PATH
。 如果不这样做,则组装也将失败。
在这些行中,我们使用实验性选择的正则表达式在所有必需的文件中用2.12替换了Scala 2.11的版本,这是因为它不足以简单地将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 GB,这可能会导致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项目结构,但依赖项和模型目录除外。
依赖关系目录包含Scala的MXNet库。 它可以通过两种方式获得:
- 在您要开发的机器上构建MXNet(请注意,该库不是跨平台的;如果您在Linux上构建它,那么它将无法在Mac OS上运行),
- 或将其从我们之前构建的Docker映像中拉出。 如果您决定在本地环境中构建MXNet,则mxnet_2.12.sh脚本将为您提供帮助。
您可以像这样从Docker镜像中提取库:
models目录包含受过训练的神经网络的文件,您可以按以下方式免费下载它们:
进一步简要介绍不特别感兴趣但在项目中起作用的文件。
项目/ build.properties
# Sbt, sbt.version = 1.2.6
项目/ plugins.sbt
src /主/资源/ 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
运行对象是应用程序的入口点。
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) {
在
部分中
您说在神经网络中它将与尺寸为1 x 3 x 512 x 512的
NDArray
一起使用,其中1是NDArray中包含的图像数,3是颜色数,而512 x 512 -图像大小(在
simple.predict.Config
对象中设置了
imageEdge = 12
的值,这是用于训练神经网络的图像的侧面大小)。 所有这些数据描述都传递给
ObjectDetector
。
另一个有趣的部分是
。
通过神经网络运行图像后,结果为
Seq[Seq[(String, Array[Float])]]
。 第一个集合仅包含一个结果(数据格式由特定的神经网络确定),然后下一个集合的每个元素都是两个元素的元组:
- 类别名称(“猫”,“狗”,...),
- 一个由五个浮点数组成的数组:第一个是概率,第二个是用于计算
x
坐标的系数,第三个是用于计算y
坐标的系数,第四个是用于计算矩形宽度的系数,第五个是用于计算矩形高度的系数。
要获得矩形坐标和尺寸的实际值,您需要将图像的原始宽度和高度乘以相应的系数。
我对
NDArray
的话题
NDArray
。 这是MXNet在给定上下文(CPU或GPU)中创建的多维数组。 创建NDArray时,会形成一个C ++对象,该对象可以非常快速地执行操作(如果在GPU上下文中创建,则几乎是瞬时的),但是您必须为此付出代价。 结果(至少在MXNet 1.3.1版本中),您需要独立管理为
NDArray
分配的内存,并且不要忘记在使用完这些对象后从内存中卸载这些对象。 否则,将会有相当大且相当快的内存泄漏,这非常不便于监视,因为用于JVM性能分析的程序看不到它。 如果您在GPU上下文中工作,则内存问题会更加严重,因为视频卡没有大量内存,并且应用程序很快崩溃而导致内存不足。
如何解决内存泄漏问题?
在上面的示例中,在将
model.imageObjectDetect(image).head map toPrediction(image.getWidth, image.getHeight) filter (_.probability > threshold)
,
imageObjectDetect
方法用于通过神经网络运行图像,该神经网络接收
BufferedImage
输入。
NDArray
所有
NDArray
转换均在方法内部完成,您无需考虑内存释放问题。 另一方面,在将
BufferedImage
转换为
NDArray
之前,
NDArray
512 x 512的大小进行
NDArray
,并使用
BufferedImage
类型的对象的方法对图像进行规范化。 例如,这种情况比使用OpenCV发生的时间更长,但是它解决了在使用
NDArray
之后释放内存的问题。
当然,您可以使用OpenCV并自己控制内存,为此,您只需要调用
dispose
的
NDArray
方法
dispose
,但是由于某种原因,您忘记了在Scala的官方MXNet文档中提及此问题。
MXNet还有一种不太方便的方法来控制由于
NDArray
发生的内存泄漏。 为此,请使用JVM参数
Dmxnet.traceLeakedObjects=true
运行应用程序。 如果MXNet注意到一个未使用但挂在内存中的
NDArray
,您将得到一个异常,指示命运多的
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 API。 该服务器基于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 ++.
, .
, . . 谢谢您的关注。
参考文献