使用Docker进行机器学习时的最佳实践

561 阅读22分钟

应用程序容器可以使用Docker 工具创建、部署和执行。它只是一个由应用程序代码和库以及其他运行所需的依赖物组成的打包包。一旦执行,Docker镜像就会变成一个容器,并包含运行一个应用程序所需的所有组件。

然而,这样做的意义何在?作为一个数据科学家或机器学习工程师,这有什么用?一个关键词,尤其是对数据科学家来说,就是可重复性。有了Docker,就可以开发一个基本的分类器,如果你的系统有一个版本的库和程序没有更新,也不能更新,因为你有某些应用程序在这些版本上操作,就可以保证很大的可重复性。这是一个体面而简单的用例。

但在我们深入挖掘之前,让我们从一些基础知识开始。

什么是Docker?

Docker是一个使用容器开发和部署应用程序的工具。容器是建立在这样一个概念上的:你可以把你的代码和它的依赖关系捆绑在一起,成为一个可部署的单元。容器已经使用了很长时间了。有些人声称它们是由Sun Microsystems创建的,并在2005年作为Solaris 10的一部分作为Zones发布,而其他人则声称BSD Jails是第一个被实现的容器技术。考虑一下用于多式联运的航运集装箱,作为一个视觉解释的例子。

一旦你把你的产品(代码和依赖关系)放在集装箱里,你可以用船或火车(笔记本或云)把它送出去,当它到达目的地时,它就像运输前一样继续运作(运行)。

货船的比喻

Cargo ship analogy

Docker--货船的比喻 |来源

有一个有用的比较,可能有助于解释Docker的工作原理。正如你可能知道的那样,物品被装在各种集装箱里运往世界各地。此外,这种隔离很重要,以便一个集装箱中的商品不会影响旁边集装箱中的商品。

为了运载不同的物品,集装箱可能是单一的尺寸和材料,也可能是不同的尺寸和材料,这取决于所运输的货物种类。

有一个经理一般负责这个登船、退船和维护船上的集装箱。在这种情况下,在你的服务器上运行的Docker引擎充当了管理者。

作为船主,你可以指示船上的经理从船上装载和卸载集装箱。同样地,docker也可以在服务器上执行、部署和关闭你的应用程序。

对于容器,你需要一张图片,作为容器的大小和内容的模板。在这里,你需要一个蓝图,即图像,从这里构建这个容器。Docker hub是一个存储镜像的好地方,而你服务器上的docker引擎将负责其余的工作。

Docker在机器学习领域发挥了什么作用?

Docker的可重复性

无论是开发模型还是进行数据分析,能够尽可能准确地重现你的结果是很重要的。考虑以下情况:你被要求对一个数据集进行探索性数据分析。你创建了一个本地环境,包括数据集和一些Python库,然后你启动了你的Jupyter笔记本。在分析了你的数据集并建立了一个初步的模型之后,你决定与你的同事和朋友分享你的发现。

问题是,你的同事要复制你的分析,他们必须也复制你的整个堆栈。Docker允许你快速、轻松地复制你的工作环境。它使项目中使用的库的版本、随机种子、甚至操作系统的标准化成为可能,这样,在不同机器上工作的同事就可以反复产生相同的结果。

Docker的可移植性

作为一名数据科学家,能够顺利地从本地工作站工作转移到提供额外资源(如CPU、RAM、GPU等)的外部集群是非常有益的。此外,我们希望能够尝试使用社区正在创建和共享的新框架和工具。

Docker使你能够将你的代码和依赖性捆绑到容器中,随后可以移植到各种计算机上,即使底层硬件、操作系统等与原来的机器不同。移动性的另一个好处是能够以相对简单的方式与各种不同的同事一起进行项目工作。

作为一个数据科学家,在你可以开始在一个既定的代码库上工作之前,你不必花一整天(或一周)来设置环境。如果项目已经被 "Docker化",你将能够立即开始工作。

在Docker中部署

Docker的使用简化了机器学习模型的部署过程。这只是你是否想与他人分享你的模型的问题。这就像把你的模型包装在一个API中,并利用Kubernetes技术把它放在一个容器中一样简单。本节中省略了几个小步骤。回顾一下,从Docker过渡到部署模型的过程是相当简单的。容器管理技术和方法可用于我们的模型部署。

在Docker中的集成

云供应商和物理服务器可以使用Docker Cloud进行配置,构建Docker节点。在你的物理服务器上安装Docker Cloud代理,或安全地连接你的云供应商凭证。然后你可以在几秒钟内 "构建节点集群"。

AWS、Azure、DigitalOcean、Packet和Softlayer都与Docker Cloud相连,用于配置和管理。要在Docker Cloud不支持的基础设施上管理节点(谷歌云、Rackspace),你也可以在连接到互联网的物理Linux系统上管理节点。

将Docker用于机器学习(ML)的最佳实践

1.构建Docker镜像

什么是Docker文件?

Docker文件是一个文本文件,包含生成特定镜像所需的所有命令,由Docker读取以自动构建该镜像。在使用docker镜像构建命令创建镜像时,应该尽可能少地进行设置或更新,这样就可以在任何时候暂停和/或销毁容器,并以很少的努力重建或替换。

什么是镜像?

使用Docker镜像对于任何使用过Docker以外的虚拟机的人来说都是很熟悉的。在其他虚拟机设置中,镜像更经常被称为 "快照"。它们是一个Docker虚拟机在某个时间和地点的快照。虽然虚拟机快照类似于Docker镜像,但有几个不同之处。

首先,Docker镜像是不可改变的。在你制作了一个记录之后,你有可能删除它,但你不能改变它。如果你需要一个新的快照,要从头开始创建一个新的镜像。

在Docker世界中,不可变性(不改变的能力)是一个有价值的工具。一旦你设置了你的Docker虚拟机并创建了一个镜像,你就可以确定这个镜像将永远发挥作用。这使得在你的环境中试验新功能变得简单。

有可能你会实验新的应用程序或重组你的文件。这样做的结果是,你可以确定你正在运行的实例将不会受到影响。你的Docker虚拟机将始终能够关闭并恢复使用你当前的镜像,就像什么都没有改变一样。

构建镜像的最佳实践

  • 使用官方镜像。在项目中,如果你是Docker的新用户,建议你使用Docker官方图片。这些照片有全面的文档,它们鼓励最佳实践,同时也迎合了最广泛的潜在应用。

修改Docker镜像的一个典型原因是为了减少镜像的大小。在安装需要优化代码的模块时,许多编程语言堆栈镜像中都包含了完整的构建工具链。一个更有经验的用户可以创建一个自定义镜像,只包括绝对必要的预编译库,从而节省磁盘空间。

  • 为了获得最佳效果,请使用一个可靠的基础镜像。当你在一个不受信任或未维护的镜像上构建容器时,你将会继承该镜像的所有缺陷和漏洞。因此,作为一个一般的经验法则,你应该遵循以下规则。

    • 受信任的存储库和供应商应优先于由不明身份的人创建的图片。
    • 在利用自定义图片时,在建立你自己的基本图片之前,请检查图片来源和Docker文件。在公共注册中心发布的图片不能保证是由Dockerfile制作的。也不能保证它在将来会被更新。
  • .dockerignore:为了防止我们在运行docker build命令时在镜像中包含不需要的文件和目录,我们可以创建一个.dockerignore文件,其中包含我们不希望在镜像中出现的文件和目录。如果不包含这个文件,应用程序的处理时间就会下降。

  • 容器必须是短暂的。短暂的容器应该由你的Dockerfile中指定的镜像生成。当它可以在重建和替换为新的容器之前被停止和销毁时,它被称为短暂性容器,需要最少的设置和配置。像短暂性容器这样的一次性容器可能会被认为是这样。对于每一个新的容器实例,与之前的容器没有任何联系。在创建Docker镜像时,你应该尽可能多地使用短暂的模式。

  • 避免安装不必要的应用程序。由于承诺以后会利用它,你可能很想安装超过你需要的东西,但为了减少镜像的大小和复杂性,只需安装实际需要的东西。例如,一旦你使用了curl包,最好将其删除,因为很多时候在构建容器时只需要它一次。

  • 把每个参数分散成几行。以 这种方式对参数进行分类有很多好处。你可以更容易地确定是否有修改,而且有助于检测包中的重复情况。

让我们在下面的例子中看看它是如何工作的!

例子一

RUN apt-get update && apt-get install -y php5-cli php5 php5-gd php5-LDAP php5-mysql apache2 php5-pgsql libapache2-mod-php5

例二

RUN apt-get update && \
    apt-get install -y \
      apache2 \
      libapache2-mod-php5 \
      php5 \
      php5-cli \
      php5-gd \
      php5-LDAP \
      php5-mysql \
      php5-pgsql

没有顺序的行是很难读的。由于需要花费大量的脑力和精力去理解它们,我们在代码审查时往往会跳过它们。那么,你认为哪种方式应该是首选?

  • 利用Docker缓存的优势。Docker镜像的一个迷人的、有帮助的功能是Layer。层是用来构建Docker镜像的,每一层都有特定的用途。Dockerfile指令与每个层有关,并描述图像在执行前和执行后的文件系统变化。各层由Docker缓存,以节省构建时间。与其重新创建一个类似的层,Docker将简单地从缓存中重用一个现有的层。

从另一个角度看,添加不是绝对必要的层会增加工作量。许多不需要的层有性能问题,因为Docker层是文件系统。用一条应用所有依赖关系的RUN命令建立一个单一的缓存,比分成许多层更有效率,因为每条运行命令都会产生一个新的层。通过寻找可缓存的层并使用它们,从长远来看,你将节省大量的时间。

  • 通过将你的断言浓缩为一个,减少层的数量。Dockerfile中的每条指令都会在镜像中增加一个层。指令和层的数量应该保持在最低限度,因为这对性能和构建时间有影响。因此,当执行RUN、COPY或ADD指令时,尝试用一句话来执行。
FROM alpine:3.4

RUN apk update
RUN apk add curl
RUN apk add vim
RUN apk add git
FROM alpine:3.4

RUN apk update && \
       apk add curl && \
       apk add vim && \
       apk add git

2.构建容器

什么是容器?

  • 应用程序可以被打包在容器中,而容器只不过是隔离的环境。容器的目标是分离并实现应用程序在多个平台上的流动性。

  • 在一个容器中,一个图像提供了所有需要的文件来操作一组进程。通过容器,应用程序与共享同一内核的其他进程隔离开来,因此没有一个进程可以影响其他进程的工作。

  • 一个集群是一个容器的集合。在一个集群中,存储和处理能力等资源可以被共享,使得同时操作几十个甚至几百个容器是可行的。

  • 容器在软件开发的背景下非常有用。DevOps方法是基于它们的适用性,这对运营和开发部门都有帮助。当涉及到开发应用程序时,容器对环境的功能是不可知的,例如操作系统,所以它们可以很容易地被共享和访问,当涉及到在同一内核上运行应用程序时,它们不像虚拟机那样复杂。

  • 尽管它们很相似,但容器不是虚拟机,因为容器可以与操作系统共享同一个内核,但虚拟机却不能。

构建容器的最佳实践

  1. 每个进程有一个容器

把容器看成是可以同时运行几个不同方面的虚拟计算机,这是一个典型的错误。容器可能以这种方式工作,但这削弱了容器架构的大部分好处。例如,同时运行标准Apache/MySQL/PHP栈的所有三个组件是很诱人的。然而,建议的方法是利用两个或三个不同的容器:一个用于Apache,一个用于MySQL,如果你使用的是PHP-FPM,可能还有一个用于PHP。

因为一个容器的寿命与它所包含的应用相同,所以每个容器只能有一个应用。当一个容器被启动时,应用程序也被启动,当应用程序被关闭时,容器也被关闭。下图描述了这一推荐做法。

Good practice
.
└── parent_process/
    ├── child process I/
    │   ├── child process I.I
    │   └── child process I.II
    └── child process II/
        ├── child process II.I
        └── child process II.II

Bad practice
.
├── parent_process I/
│   ├── child process I.I
│   └── child process I.II
--------------------------	
└── parent process II/
    ├── child process II.I
    └── child process II.II

建议每个容器只运行一个进程,有几个原因。

  • 比方说,你的应用架构中有两个Web服务器和一个数据库。可以在一个容器中运行所有三个,但最好是在一个不同的容器中运行每个服务,这样可以更简单地重复使用和增长。
  • 每个服务都有一个独立的容器,这样你就可以水平地增长你的一个网络服务器,以适应额外的流量。
  • 你可能在未来需要一个容器化的数据库用于其他服务。在这种情况下,同一个数据库容器可以被重复使用,而不需要额外的两个服务。
  • 当容器被耦合时,日志变得明显更困难。
  • 由于要处理的表面积较小,产生安全更新或故障排除就会简单得多。

2.标记你的容器

当涉及到维护图像时,Docker标签是一个非常有用的工具。它可以帮助维护多个版本的docker镜像。下面是一个构建标签名称为v1.0.1的docker镜像的例子。

docker build -t geekflare/ubuntu:v1.0.1

有几个标签你可以使用。

  • 使用 稳定标签来保留容器的基本镜像。避免将这些标签用于部署容器,因为这些标签会经常被改变,而且可能会导致生产环境中的差异。
  • 使用 唯一标签用于部署。使用独特的标签,你可以轻松地将你的生产集群扩展到许多节点。它可以防止不一致,而且主机不会获取任何其他docker镜像版本。

推荐的做法是在部署的镜像标签上禁用写入功能。这可以防止部署的镜像被意外地从注册表中删除。

3.在Docker中运行你的模型

为什么要把机器学习模型放在Docker中?

挑战

  • 构建一个在我们的PC上运行的机器学习模型的过程并不困难,但与一个想在不同类型的服务器上大规模利用该模型的客户合作,可能真的很有挑战性。可能会出现各种顾虑,包括性能问题、程序崩溃和优化不良。

  • 我们的机器学习模型可以用Python这样的单一编程语言来实现,但该应用程序需要与其他用其他编程语言编写的应用程序进行数据接收、预处理、前端等方面的沟通,这本身就是一个挑战。

使用Docker的好处

  • 由于每个微服务都可能使用独立的语言,Docker实现了可扩展性和独立服务的简单添加或删除。可复制性、可移植性、易于部署、增量更新、轻量级和简单性是Docker的一些好处。
  • 数据科学家最担心的是他们的模型不能准确地反映现实生活中的结果,或者在与其他研究人员分享时不能准确地反映。在某些情况下,模型并不是问题,而是需要复制整个堆栈。使用Docker,可以在任何系统上复制训练和操作机器学习模型所需的工作环境。
  • 通过Docker,代码和依赖关系可以一起打包成可移植的容器,可以在各种不同的主机上运行,而不考虑硬件或操作系统。
  • 训练模型可以在本地系统上构建,并简单地转移到拥有更多资源(如GPU、更多内存或强大CPU)的远程集群。OpenShift,一个Kubernetes发行版,使得部署变得简单,通过将你的模型包装成容器中的API,并使用这样的技术部署容器,使你的模型能够被世界所访问。
  • 容器镜像的版本可以用Docker来跟踪,也可以跟踪谁开发的,用什么开发的。版本也可以被回滚。此外,即使你的机器学习应用所依赖的某项服务正在升级、修复或不可用,它也可以运行。例如,如果整个解决方案中包含的一个输出信息需要更新,就不需要与其他服务互动,也不需要更新整个程序。

4.跟踪你的模型的指标

即使在模型开发之后,机器学习(ML)的生命周期也远远没有结束;事实上,这仅仅是这个过程的开始。为了将你的模型投入生产,下一步是部署和监测你的模型。为了最大限度地利用你的机器学习模型,只要你在使用它,你就需要一直盯着它。

将ML模型监控视为与频繁的健康检查一样。作为一项业务职责,模型监测帮助你确保你的模型在最佳状态下工作。

模型监控的重要性

监测你的ML模型的错误、崩溃和延迟是机器学习生命周期中模型部署后的操作阶段的一部分。

模型漂移,或模型在使用过程中的退化,是模型监控有必要的最直接原因。一个模型的预测能力可能会下降的原因有以下几点

  1. 以前未见过的信息。如果没有足够的标记数据,或者对训练有计算上的限制,就不可能避免数据样本中你的机器学习模型产生的结果是错误的或者是错的。
  2. 变量连接和周围环境的变化。ML模型的优化取决于它们被建立时的变量和参数。考虑一下二十年前开发的垃圾邮件检测算法现在有多有效。二十年前,互联网甚至还不存在!而现在,互联网已经成为了一个重要的组成部分。这种方法可能无法跟上垃圾邮件不断变化的本质,因此,它是否有效是值得怀疑的。环境的变化在这个例子中得到了体现。
  3. 上游数据的变化。这是一个用来描述对操作数据管道的修改的术语。数据科学团队通常不能完全控制输入数据所来自的所有系统。虽然数据是以摄氏度记录的,但一个完全隔离的软件工程团队可能会把一个变量的测量结果改为华氏度。例如,如果有人试图确定平均温度,这可能会产生严重的影响。

ML实验跟踪工具

由于上一节中提到的原因,很明显,机器学习模型的暂存环境永远不等同于生产环境,因此你必须对你投入生产的任何ML模型进行跟踪。

现在有很多技术可以帮助我们做到这一点。首先,我们有知名的工具,如MLFlowTensorBoardNeptuneDVC、*Amazon SageMaker,*等等。

为了获得更好的理解,让我们看看Neptune,看看我们如何通过这个工具在我们的模型通过docker部署后保持检查。

海王星的简短概述

Neptune是一个用于MLOps的元数据存储,为运行大量实验的团队而建。

它为你提供了一个单一的地方来记录、存储、显示、组织、比较和查询你所有的模型构建元数据。

Neptune用于。

  • 实验跟踪。在一个地方记录、显示、组织和比较ML实验。
  • 模型注册表。版本、存储、管理和查询训练好的模型和模型构建元数据。
  • 实时监控ML运行。记录并监控模型训练、评估或生产的实时运行。

如果你想了解更多,请查看文档 或探索应用程序中的一个示例项目(无需注册)。

使用Neptune的Docker

让我们按照一个逐步的过程来看看我们如何在容器化的Python脚本或应用程序中记录实验元数据

第1步:将Neptune添加到你的代码中

让我们训练一个简单的线性回归模型并把它放在Docker容器中。

# training.py

import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split
import joblib
import neptune.new as neptune
from neptune.new.types import File
import os


run = neptune.init(
    project="PROJECT_WORKSPACE/PROJECT_NAME",
    api_token="YOUR_API_TOKEN",
    tags='a_tag_to_help_you'
) 

lr = LinearRegression()
# #############################################################################
# Load and split data
for _ in range(100):
    rng = np.random.RandomState(_)
    x = 10 * rng.rand(1000).reshape(-1,1)
    y = 2 * x - 5 + rng.randn(1000).reshape(-1,1)

    X_train, X_test, y_train, y_test = train_test_split(x, y, test_size=0.2, random_state=123)

    # #############################################################################
    # Fitting the model

    lr.fit(X_train, y_train)
    test_mse = mean_squared_error(y_test, lr.predict(X_test))


    run['test/mse'].log(test_mse)

joblib.dump(lr, '/any_name.pkl')
run['model'].upload(File.as_pickle('any_name.pkl'))

为了观察我们的模型在不同数据下的表现,我们运行100次,每次都使用Numpy的.RandomState()的不同随机数据。因此,每次我们都会收到一个更新的指标值,用.log函数记录下来。

现在,lr是我们的模型,test_mse是我们的度量。我们如何让这个相同的结果在不同的机器上重现呢?我们使用Docker!

第二步:更新requirements.txt文件并创建Docker文件

由于现在我们使用的是Neptune包,所以我们需要将其纳入我们的需求文件中。

# requirements.txt
joblib==1.1.0
neptune-client
numpy==1.21.4
scikit-learn==1.0.1

现在创建一个Dockerfile,它将。

  • 指定基础容器镜像,我们将从该镜像中构建我们的容器。
  • 在更新的requirements.txt中安装依赖项
  • 将我们的训练脚本复制到容器镜像中,并定义命令来执行它。
# syntax=docker/dockerfile:1
FROM python:3.8-slim-buster

RUN apt-get update
RUN apt-get -y install gcc

COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt

# Copy all files in the current dir to the main dir of the container
COPY . .
CMD [ "python3", "-W ignore" ,"training.py" ]

第3步:通过API令牌构建并运行docker容器

通过运行以下命令,从上一步创建的Docker文件中构建一个docker镜像。

# image-name: neptune-docker
docker build --tag <image-name> . 

现在,我们用我们的凭证运行容器。

# NEPTUNE_API_TOKEN: YOUR_API_TOKEN
# image-name: neptune-docker 
docker run -e NEPTUNE_API_TOKEN="<YOUR_API_TOKEN>" <image-name>

在你运行docker容器后,你会在终端上得到一个链接。点击该链接,在Neptune中打开运行,观看你的模型训练实况。

Monitor model training runs live in Neptune

监控模型训练在Neptune中的实时运行

在我们的例子中,我们记录的是MSE,这可以改成任何对你的模型和用例有意义的指标。

总结

互联网上的许多教程和文章都只关注如何建模和理解指标的问题。一个著名的科学家或一个小公司的雇员在科学过程中都要经过同行的审查。

密切关注你的模型在新鲜数据和变化下的表现。即使你对学习MLOps不感兴趣,你也无法从强化课程中获益。我希望你能发现这些材料对你的日常工作有帮助。