如何用Docker成为更高效的数据科学家?
使用 Docker 容器来开发机器学习模型的好处有很多。近日,GitHub 的资深机器学习科学家 Hamel Husain 在 Towards Data Science 上发表了一篇入门级的 Docker 容器教程,文章从基本的概念谈起,清楚明白地介绍了 Docker 容器的一些基本的操作方式和注意事项。机器之心对本文进行了编译介绍。本文所涉及的所有相关代码请访问:https://github.com/hamelsmu/Docker_Tutorial。
过去五年来,Docker 容器已然成了一个热门词汇,似乎我的所有软件工程师朋友都在使用它们来开发应用。我想搞清楚这种技术可以如何让我更有效率,但我发现我在网上找到的教程要么过于注重细节(解释了一些我作为数据科学家绝不会使用的功能),要么就过于浅显(没有足够的信息帮助我理解如何快速有效地使用 Docker)。
所以我写了这篇快速入门,这样你不必自己去网上筛选信息就能学习到快速上手 Docker 所需要的一切。
Docker 是什么?
你可以把 Docker 看作是轻量级的虚拟机——包含你运行应用所需要的一切。Docker 容器可以获取你的系统的状态的快照,这样其他人就可以使用这个快照快速重建你的计算环境。对于本教程而言,这就是你需要了解的一切。更多详细介绍可参阅:https://goo.gl/YzUwbc。
为什么要使用 Docker?
1. 重现性:作为专业的数据科学家,让你的结果能够重现是非常重要的。重现性不仅有助于同行评议,而且可以确保你创建的模型、应用或分析可以无障碍地运行,这能让你交付的成果更稳健,更能经受时间的考验。举个例子,假如你用 Python 创建了一个模型,只是运行 pip freeze 并将结果得到的 requirements.txt 文件发送给你的同事是不够的,因为其中只包含特定于 Python 的依赖条件——而实际上的依赖条件不只有 Python,还有操作系统、编译器、驱动程序、配置文件以及你的代码成功运行所需的其它数据。就算你只分享 Python 依赖条件也能成功,将所有东西都封装到一个 Docker 容器中还是能减轻其他人重建你的环境的负担,并让他们能更轻松地访问你的成果。
2. 计算环境的可移植性:作为一位数据科学家,尤其是机器学习领域内的数据科学家,快速改变你的计算环境的能力能够极大地影响你的生产力。数据科学的开始工作常常是原型设计、探索和研究——这些工作并不一定立即就需要特定的计算资源。这个工作往往是在笔记本电脑或个人计算机上完成的。但是在后面某个时候,你往往会需要不同的计算资源来显著加速你的工作流程——比如使用更多 CPU 或强大的 GPU 来执行深度学习等任务。我看到很多数据科学家由于感受到了在远程机器上重建他们的本地环境的困难,就将自己局限在了本地计算环境内。而 Docker 能让你的环境(你的所有库和文件等等)的移植非常简单。在 Kaggle 竞赛中,快速移植计算环境也是一个巨大的竞争优势,因为你可以成本高效地利用 AWS 的宝贵计算资源。最后,创建 Docker 文件让你能移植很多你喜欢的本地环境配置——比如 bash 别名或 vim 插件。
3. 强化你的工程能力:熟练使用 Docker 让你能将模型或分析部署成应用(比如用作提供预测的 REST API),从而让其他人也能使用你的成果。此外,你在数据科学工作流程中可能需要与存在于 Docker 容器中的其它应用进行交互,比如数据库。
Docker 术语
在我们继续深入之前,熟悉一下 Docker 的术语会很有帮助:
- 镜像(image):是你想要创建的东西的蓝图。比如:Ubuntu+TensorFlow,带有英伟达驱动程序和一个运行的 Jupyter 服务器。
- 容器(container):是你实现的运行的镜像的实例化。你可以运行同一个镜像的多个副本。分清镜像和容器之间的差异非常重要,因为这是新入门者常常混淆的两个概念。如果你不清楚镜像和容器的差别,停下来再读一次。
- Dockerfile:用于创建镜像的配方。Dockerfile 包含特殊的 Docker 语法。官方文档说:Dockerfile 是一个文本文档,其中包含了用户可以在命令行调用的用来组装成镜像的所有命令。
- commit:和 git 类似,Docker 容器提供了版本控制。通过 commit 发生的改变,你在任何时间都可以将你的 Docker 容器的状态保存为一个新镜像。
- DockerHub/Image Registry:人们可以发布公开(或私人)Docker 镜像的地方,用于促进合作与共享。
- 层(layer):对已有镜像的修改,由 Dockerfile 中的一个指令表示。层按次序应用到基础镜像上,以创建出最终的镜像。
本文将使用这些术语,如果你在阅读时忘记了,一定要回来查看!这些术语很容易混淆,尤其是在镜像和容器之间——所以你在阅读时要保持警惕!
安装 Docker
你可以免费下载安装 Docker 社区版(Docker Community Edition),地址:https://www.docker.com/community-edition
创建你的第一个 Docker 镜像
在创建 Docker 容器之前,创建一个将用于定义镜像的 Dockerfile 会很有用。我们先慢慢解读一下下面的 Dockerfile。你也可以在与本教程关联的 GitHub 库中找到这个文件:https://goo.gl/iE4Bdr。
-
# reference: https://hub.docker.com/_/ubuntu/
-
FROM ubuntu:16.04
-
-
# Adds metadata to the image as a key value pair example LABEL version="1.0"
-
LABEL maintainer="Hamel Husain <[email protected]>"
-
-
##Set environment variables
-
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8
-
-
RUN apt-get update --fix-missing && apt-get install -y wget bzip2 ca-certificates \
-
build-essential \
-
byobu \
-
curl \
-
git-core \
-
htop \
-
pkg-config \
-
python3-dev \
-
python-pip \
-
python-setuptools \
-
python-virtualenv \
-
unzip \
-
&& \
-
apt-get clean && \
-
rm -rf /var/lib/apt/lists/*
-
-
RUN echo 'export PATH=/opt/conda/bin:$PATH' > /etc/profile.d/conda.sh && \
-
wget --quiet https://repo.continuum.io/archive/Anaconda3-5.0.0.1-Linux-x86_64.sh -O ~/anaconda.sh && \
-
/bin/bash ~/anaconda.sh -b -p /opt/conda && \
-
rm ~/anaconda.sh
-
-
ENV PATH /opt/conda/bin:$PATH
-
-
RUN pip --no-cache-dir install --upgrade \
-
multiprocessing \
-
sklearn-pandas
-
-
# Open Ports for Jupyter
-
EXPOSE 7745
-
-
#Setup File System
-
RUN mkdir ds
-
ENV HOME=/ds
-
ENV SHELL=/bin/bash
-
VOLUME /ds
-
WORKDIR /ds
-
ADD run_jupyter.sh /ds/run_jupyter.sh
-
RUN chmod +x /ds/run_jupyter.sh
-
-
# Run a shell script
-
CMD ["./run_jupyter.sh"]
FROM 语句
-
FROM ubuntu:16.04
FROM 语句包含了 Docker 最神奇的部分。这个语句指定了你想在上面进行创建的基础镜像。通过使用 FROM 指定一个基础镜像,Docker 将会在你的本地环境中寻找名为 ubuntu:16.04 的镜像——如果它没有找到,它就会搜索你指定的 Docker Registry,默认是 DockerHub:
https://hub.docker.com/explore/。如果你需要经常在你的 Ubuntu 等操作系统上安装程序,那么这种分层机制就非常方便。你不必费心从头开始安装 Ubuntu,而是可以直接在官方的 Ubuntu 镜像上开发!DockerHub 上托管着种类繁多的镜像,包括那些不只是提供了一个操作系统的镜像,比如如果你想要一个已经安装了 Anaconda 的容器,你可以选择在官方的 Anaconda Docker 镜像上开发,地址:https://hub.docker.com/r/continuumio/anaconda3/。最重要的是,你也可以随时发布你构建的镜像,即使该镜像是通过在其它镜像上加层得到的!这有无尽的可能性。
在这个案例中,我们指定基础镜像为 ubuntu:16.04,它会搜索名叫 ubuntu 的 DockerHub 库(https://hub.docker.com/_/ubuntu/)。镜像名之后的部分 16.04 是指定了你想要安装的基础镜像的版本的标签(tag)。如果你检索一下 Ubuntu DockerHub 库,你会注意到不同版本的 Ubuntu 对应于不同的 tag:
2017 年 12 月的官方 Ubuntu DockerHub 库截屏
比如,ubuntu:16.04、ubuntu:xenial-20171201、ubuntu:xenial 和 ubuntu:latest 全都是指 16.04 版的 Ubuntu,它们全都是同一个镜像的别名。此外,这里提供的链接指向了对应的 Dockerfile,可用于构建每个版本的镜像。有时候你无法在 DockerHub 中找到 Dockerfile,因为维护者可以自己选择是否将关于这些镜像的创建方式的 Dockerfile 包含进来。我个人觉得阅读一些 Dockerfile 有助于更好地理解 Dockerfile。(但不要急,读完这篇教程再说!)
你需要特别注意一个标签,即 :latest 标签。这也是你在不为 FROM 语句指定标签时默认 pull 的镜像。比如说如果你的 FROM 语句是这样:
-
FROM ubuntu
然后你就将 pull ubuntu:16.04 镜像。为什么?——仔细看上面,你可以看到 :latest 关联的是 16.04.
关于 Docker 镜像最后需要注意的一点:在从 DockerHub pull 随机的 Docker 镜像时要做出明智的判断。有恶意的人创建的镜像有可能会包含恶意软件。
LABEL 语句
这个语句会为你的镜像添加元数据,而且是完全可选的。我增加这个语句的目的是为了让别人知道可以联系谁,同时也方便我搜索我的 Docker 容器,尤其是在一个服务器上同时运行着很多容器时。
-
LABEL maintainer="Hamel Husain <youremail>"
ENV 语句
-
ENV LANG=C.UTF-8 LC_ALL=C.UTF-8
这让你可以修改环境变量,而且相当直接,相关情况请参阅:
https://docs.docker.com/engine/reference/builder/。
RUN 语句
这通常是最需要花功夫的地方,给出了你构建该 Docker 镜像所想要完成的任务。你可以运行 apt-get 和 pip install 等任意的 shell 命令来安装你需要的软件包和依赖包。
-
RUN apt-get update --fix-missing && apt-get install -y wget bzip2
-
build-essential \
-
ca-certificates \
-
git-core \
-
-
...
在这里我安装了一些我喜欢的实用工具,比如 curl、htop、byobu,然后安装了 Anaconda,之后还安装了一些基础 Anaconda 中没有的其它库(你可以在完整的 Dockerfile 中查看其它 RUN 语句)。
RUN 语句后的命令与 Docker 没什么关系,只是一些你在安装这些软件包时需要运行的正常 Linux 命令,所以就算你不熟悉这些软件包或 Linux 命令也不要担心。另外,再给一个建议:当我最早开始学习 Docker 时,我查看了 GitHub 或 DockerHub 上的其它 Dockerfile,然后将我需要的部分复制粘贴到了我的 Dockerfile。
你可能注意到了 RUN 语句的格式。每个库或软件包都整齐地进行了缩进,而且为了可读性还按字母进行了排序。这是 Dockerfile 的普遍惯例,所以我建议你也这样做以便合作。
EXPOSE 语句
如果你想公开一个端口,这个语句会很有用——比如,如果你从该容器或某个网络服务内实施一个 Jupyter Notebook。Docker 的文档相当好地解释了 EXPOSE 语句:
EXPOSE 指令实际上并没有发布该端口。它的功能是作为创建该镜像的人和运行该容器的人之间的一类文档,内容是关于打算发布的端口。要实际发布该端口,就要在运行该容器时在 docker run 上使用 -p 标志并且映射一个或多个端口,或者也可以使用 -P 标志发布所有端口并将它们映射到高阶端口。
VOLUME 语句
-
VOLUME /ds
这个语句让你可以在 Docker 容器和主机计算机之间共享数据。VOLUME 语句让你可以安装外部安装的卷。主机目录只有在容器运行时才声明(因为你可能在不同的计算机上运行该容器),而不会在定义镜像时声明*。目前你只指定了 Docker 容器内你想与主机容器共享的文件夹的名称。
Docker 用户指南解释说:
*主机目录是在容器运行时声明的:主机目录(挂载点)本质上取决于主机。这是为了保证镜像的可移植性,因为一个给定的主机目录无法保证在所有主机上都可用。由于这个原因,你不能在 Dockerfile 中挂载主机目录。VOLUME 指令不支持指定 host-dir 参数。你必须在创建或运行容器时指定挂载点。
此外,这些卷的目的是将数据保存到容器的文件系统之外,当你要操作大量数据而且不希望你的镜像膨胀得很大时,这会很有用。当你保存一个 Docker 镜像时,在这个 VOLUME 目录中的任何数据都不会被保存为该镜像的一部分,但是在这个容器目录之外的数据会被保存。
WORKDIR 语句
-
WORKDIR /ds
这个语句设置了工作目录,以便你在另一条命令中可以无需使用绝对路径就能索引特定的文件。例如这个 Dockerfile 中的最后一条语句是:
-
CMD [“./run_jupyter.sh”]
该语句就默认假设工作目录是 /ds
ADD 语句
-
ADD run_jupyter.sh /ds/run_jupyter.sh
这条命令让你可以在 Docker 容器运行时将文件从主机计算机复制到该 Docker 容器。我使用这个命令来执行 bash 脚本以及将 .bachrc 文件等有用东西导入到容器中。
注意这里的主机容器的路径并没有完全指定,因为其主机路径是你在该容器运行时指定的背景路径(context directory)的相对路径(后面会讨论)。
在我运行这个容器时,run_jupyter.sh 正好在背景路径的根目录内,所以在该源文件之前没有路径。
用户指南中介绍说:
ADD ... ADD 指令从 复制新文件、目录或远程文件 URL 并将它们添加到路径 的镜像的文件系统中。
CMD 语句
Docker 容器的设计思想是这些容器是短暂的,能保证运行完你想运行的应用就行了。但在数据科学方面,我们往往希望保持这些容器一直运行,即使它们之中并没有主动地运行着什么。很多人都通过运行 bash shell 来实现这一点(除非你终止它,否则它就不会停止)。
-
CMD [“./run_jupyter.sh”]
在上面的命令中,我运行了一个实例化一个 Jupyter Notebook 服务器的 shell 脚本。但是,如果你没有什么要运行的特定应用而只是想保持你的容器运行(而不退出),你可以直接运行 bash shell,只不过使用以下命令:
-
CMD ["/bin/bash"]
这种方法是有效的,因为除非你退出,否则 bash shell 就不会终止;因此该容器会一直保持运行。
用户指南中介绍说:
在一个 Dockerfile 中只能有一个 CMD 指令。如果你列出了不止一个 CMD,那么只有最后一个才有效。CMD 的主要目的是为正在执行的容器提供默认配置。这些默认配置可能包含一个可执行文件,或者也可以省略可执行文件,在这种情况下你还必须指定一个 ENTRYPOINT 指令。
创建你的 Docker 镜像
Dockerfile 中的信息可真够多的。不要担心,后面的内容就相对很简单了。现在我们已经在 Dockerfile 中创建了我们的配方,是时候创造镜像了。你可以通过以下命令完成:
GitHub 上也有:https://github.com/hamelsmu/Docker_Tutorial/blob/master/basic_tutorial/build_image.sh
这会创建一个 Docker 镜像(而不是容器;如果你不记得这两者之间的差异,请查阅文章前面的术语介绍),你可以在后面运行这个镜像。
从你的 Docker 镜像创建和运行容器
现在你已经准备好让这一切工作起来了!我们可以通过执行以下命令来调出环境:
同样 GitHub 也有:https://github.com/hamelsmu/Docker_Tutorial/blob/master/basic_tutorial/run_container.sh
运行完这个命令之后,你的容器就运行起来了!Jupyter 服务器也运行起来了,因为在该 Dockerfile 最后有这个命令:
-
CMD [“./run_jupyter.sh”]
现在你应该可以通过其使用的端口访问你的 Jupyter Notebook 了——在这个案例中可通过 http://localhost:7745/ 访问,密码是 tutorial。如果你是通过远程的方式运行这个 Docker 容器,你还必须设置本地端口转发,这样你才能通过你的浏览器访问你的 Jupyter 服务器。端口转发介绍:https://help.ubuntu.com/community/SSH/OpenSSH/PortForwarding
与你的容器交互
一旦容器设置完成并运行起来,下面这些命令就有用了:
- 为容器附加一个新的终端会话。如果你需要安装一些新软件或使用 shell,这会很有用。
- 将你的容器的状态保存为新镜像。即使你一开始就在 Dockerfile 中配置了你想安装的所有库,随着时间的推移,你也可能还是需要对容器的状态进行很大的调整——通过交互来增加更多库和软件包。将你的容器的状态保存为镜像是很有用的,你后面可以将其分享出去或在上面加层。你可以使用 docker commit CLI 命令将容器状态保存为新镜像:
-
docker commit <container_name> new_image_name:tag_name(optional)
比如说,如果我想将名为 container1 的容器的状态保存为名为 hamelsmu/tutorial:v2 的镜像,我可以直接运行这个命令:
-
docker commit container_1 hamelsmu/tutorial:v2
你可能会疑惑镜像名之前的 hamelsmu/ 是什么——这只是为了让之后将该容器推送到 DockerHub 的工作更轻松,因为 hamelsmu 是我的 DockerHub 用户名(后面会再谈这个问题)。如果你的工作要使用 Docker,那么你的公司很可能有一个内部私有的 Docker 库,你也可以将你的 Docker 推送到那里。
- 列出运行中的容器。当我忘记现在正在运行的容器的名称时,我就常常使用这个命令:
-
docker ps -a -f status=running
如果你在使用该命令时没有加上 status=running,那么你就会看到你系统上的所有容器的列表(即使已经不再运行的容器也在)。这对查找旧容器而言很有用。
- 列出你在本地保存的所有镜像。
-
docker images
- 将你的镜像推送到 DockerHub(或其它地方)。如果你想与其他人分享你的工作或将镜像保存到云上,这个命令就会很有用。注意你在做这件事时可不要分享任何私人信息(DockerHub 上也有私有库)。
首先创建一个 DockerHub 库并给你的库起一个适当的名称,参考这里:
https://docs.docker.com/docker-hub/repos/。然后要运行 docker login 命令来连接到你在 DockerHub 或其它注册位置的账户。比如,要推送一个镜像到这个容器
(https://hub.docker.com/r/hamelsmu/tutorial/),我首先必须将我的本地镜像命令为 hamelsmu/tutorial(我可以选择任意标签名)。比如说,这个 CLI 命令就为:
-
docker push hamelsmu/tutorial:v2
将之前提到的 Docker 镜像推送到这个库,其标签为 v2,参考:https://hub.docker.com/r/hamelsmu/tutorial/tags/。需要指出:如果你公开了你的镜像,那么其他人就可以直接在你的镜像上加层,就像本教程中我们在 ubuntu 镜像上加层一样。对于想要重现或延展你的研究的其他人来说,这非常有用。
你已经掌握了
现在你知道如何操作 Docker 了,你可以执行以下任务:
- 与同事和朋友共享可重现的研究。
- 通过将你的代码暂时迁移到所需的更大的计算环境中,无中断地赢得 Kaggle 竞赛。
- 在你的笔记本电脑上的 Docker 容器内进行本地的原型开发,然后毫不费力地将同样的计算过程无缝迁移到服务器上,同时还能保留你喜欢的本地环境配置(你的别名、vim 插件、bash 脚本、自定义提示等)。
- 使用 Nvidia-Docker 在 GPU 计算机上快速实例化运行 TensorFlow、PyTorch 或其它深度学习库所需的所有依赖包。(如果你从头开始做,这个过程将非常艰辛。)参阅后面的彩蛋。
- 将你的模型作为应用发布,比如用作从 Docker 容器提供预测的 REST API。当你的应用 Docker 化了以后,就可以按照需要轻松地随意复制。
进阶阅读
到这里我们也只学到了 Docker 的一点皮毛,前面还有很多东西值得掌握。我很关注 Docker 领域,我认为数据科学家会常常遇到它,希望这篇文章能让你有足够的信心开始使用它。下面这些资源曾在我的 Docker 之旅中为我提供过帮助:
- 有用的 Docker 命令:https://zaiste.net/posts/removing_docker_containers/
- 更有用的 Docker 命令:https://www.digitalocean.com/community/tutorials/how-to-remove-docker-images-containers-and-volumes
- Dockerfile 参考:https://docs.docker.com/engine/reference/builder/
- 如何创建和推送到 DockerHub 上的库:https://docs.docker.com/docker-hub/repos/
彩蛋:Nvidia-Docker
我学习 Docker 最早的原因是要在单个 GPU 上做深度学习模型的原型开发,然后在我需要更多计算资源时再迁移到 AWS 上。我当时也在学习 Jeremy Howard 的出色的 Fast.AI 课程(http://www.fast.ai/),并且希望与其他人分享我的原型设计。
但是,要将英伟达 GPU 的驱动程序等所有依赖包都包含以来,你不能使用 Docker,而是要用 Nvidia-Docker(https://github.com/NVIDIA/nvidia-docker)。这比使用 vanilla Docker 要多花一些功夫,但只要你理解了 Docker,做起来就很简单。
我将我的 Nvidia-Docker 设置放在这里:https://github.com/hamelsmu/Docker_Tutorial/tree/master/gpu_tutorial,你可以用这个来进行练习