¿Qué es y porqué usar Docker en el Machine Learning?

En este post veremos qué es Docker y por qué es necesario en el Machine Learning Engineering. También veremos una guía para que de forma sencilla puedan incorporar Docker en su flujo de trabajo cuando sea necesario.

¡Así que listo, comencemos!

Video

Como siempre, en el canal de YouTube se encuentra el video de este post:

Introducción

En las etapas experimentales de desarrollo de un modelo de Machine Learning podemos implementar el prototipo en un computador personal. Pero en las etapas de desarrollo y producción del modelo usualmente requerimos la colaboración de un equipo, así como llevar el modelo a la nube para que podamos llegar a varios usuarios de la aplicación.

Pero esta aplicación muy probablemente no será definitiva, porque en este caso el Machine Learning es un proceso iterativo: los datos pueden cambiar, el modelo mismo puede cambiar, y por tanto cambiará el producto final.

Así que durante este el proceso comienzan a surgir varios inconvenientes, pues debemos garantizar que la aplicación pueda ser ejecutada fácilmente por cualquier miembro del equipo o desde un servidor, independientemente del tipo de hardware o software que se use en estos equipos remotos.

Una solución es “empaquetar” nuestra aplicación en algo que se conoce como un contenedor, para así poderla distribuir de forma mucho más eficiente. En la actualidad, Docker es una de las plataformas más usadas para esto.

Si queremos conseguir un empleo en el área del Machine Learning, definitivamente Docker es una de esas herramientas que debemos manejar. Pues en este artículo veremos todos los detalles de Docker, y entenderemos por qué resulta fundamental en el Machine Learning Engineering.

Los inconvenientes al distribuir una aplicación de Machine Learning

Partamos de un ejemplo hipotético para entender porqué el uso contenedores de software, como Docker, resultan esenciales en el Machine Learning Engineering.

Supongamos que estamos desarrollando una aplicación de Machine Learning inicialmente en nuestro entorno local (es decir en nuestro computador). Instalamos todas las librerías requeridas, optimizamos el código para que aproveche la CPU y la GPU de nuestro computador, entrenamos el modelo y lo tenemos listo y funcionando a la perfección:

Características de nuestra aplicación desarrollada localmente
Características de nuestra aplicación desarrollada localmente

Pero luego compartimos el desarrollo con otro colega, o lo llevamos a otro computador, o intentamos desplegarlo en la nube, usando entornos con una configuración similar, pero resulta que no funciona. Comienzan a aparecer errores, conflictos con las versiones de las librerías, o con el hardware. Y nos damos cuenta que realmente resulta muy pero muy complicado depurar todos estos errores y poner a funcionar el prototipo en un entorno diferente al nuestro:

Al intentar distribuir la aplicación (es decir lograr que funcione en un equipo remoto) encontramos varios errores
Al intentar distribuir la aplicación (es decir lograr que funcione en un equipo remoto) encontramos varios errores

Incluso, es posible que funcione, pero de repente en el equipo remoto (o en el servidor) se actualizó un driver y la aplicación deja de funcionar o corre más lento de lo normal.

Y el problema es aún más complejo si estamos en un entorno laboral, donde tenemos un equipo de desarrolladores y queremos hacer varias pruebas sobre el modelo en la etapa de desarrollo, antes de llevarlo a la etapa de producción y montarlo en un servidor para dar acceso a los usuarios.

¿Pero por qué tantos inconvenientes? Pues, si analizamos por un momento la aplicación, nos daremos cuenta que el código desarrollado (por nosotros, o por el equipo de desarrollo) es sólo la punta del iceberg.

En realidad el problema es más complejo de lo que parece. El código está montado en una API (como Tensorflow o Pytorch) que evoluciona rápidamente, y esta API a su vez tiene otras dependencias muchas de las cuales son proyectos independientes (como Numpy o incluso Python) que también cambian constantemente. Y para enredar aún más las cosas todas estas librerías usan rutinas compiladas específicamente para la CPU local, así como para la GPU y los drivers de la GPU.

Así que cuando movemos el código del equipo local al de nuestros colaboradores o a un cluster en la nube, lo que estamos haciendo es introducir múltiples puntos de falla lo que quiere decir que es altamente probable que no exista coincidencia entre las librerías y dependencias usadas por el equipo local y el remoto.

Los puntos de falla en la distribución se deben a que no hay coincidencia entre el software, las librerías y/o las dependencias del entorno local y del remoto
Los puntos de falla en la distribución se deben a que no hay coincidencia entre el software, las librerías y/o las dependencias del entorno local y del remoto

Para resolver todos estos inconvenientes necesitamos una herramienta que cumpla tres condiciones:

  1. Que nos dé resultados consistentes, es decir que nos permita ejecutar la aplicación de Machine Learning en diferentes computadores y siempre con los mismos resultados.
  2. Que nos dé portabilidad, es decir que resulte sencillo desarrollar la aplicación y “empaquetarla” fácilmente para desplegarla, por ejemplo, en la nube.
  3. Y que se encargue en su totalidad del manejo de dependencias, es decir que “empaquete” no sólo el código sino exactamente las mismas versiones de las librerías y dependencias usadas localmente.

Docker: una solución al problema de la distribución de aplicaciones

Y acá es donde entra al rescate Docker, que es actualmente la plataforma más usada para la creación de contenedores de software.

Usemos una metáfora para entender lo que hace Docker. Cuando un barco de carga transporta mercancía lo hace usando varios contenedores. Cada contenedor tiene un producto diferente, y esto se hace para evitar que diferentes productos, de diferentes proveedores y con diferentes características, se mezclen y se arme un desorden:

Los contenedores en un barco de carga se usan para aislar un tipo de mercancia de otra, facilitando así su transporte
Los contenedores en un barco de carga se usan para aislar un tipo de mercancia de otra, facilitando así su transporte

Pues docker hace lo mismo que estos contenedores del barco, sólo que en lugar de productos tenemos el código y todas las dependencias asociadas a nuestra aplicación:

Docker funciona de forma similar a la de un barco de carga: permite encapsular diferentes aplicaciones para facilitar su distribución a equipos o servidores remotos
Docker funciona de forma similar a la de un barco de carga: permite encapsular diferentes aplicaciones para facilitar su distribución a equipos o servidores remotos

En detalle, lo que hace Docker es crear un contenedor que encapsula por completo no sólo el código, sino la totalidad de las dependencias, llegando hasta las librerías que interactúan con el hardware para acceder por ejemplo a puertos de red y a recursos de CPU y GPU:

Docker crea un contenedor que encapsula los elementos esenciales de la aplicación. En la figura estos elementos son las librerías de la GPU y la CPU, todas las librerías asociadas a Tensorflow 2.0 y, por supuesto, el código desarrollado
Docker crea un contenedor que encapsula los elementos esenciales de la aplicación. En la figura estos elementos son las librerías de la GPU y la CPU, todas las librerías asociadas a Tensorflow 2.0 y, por supuesto, el código desarrollado

Y con esto se logra estandarizar el entorno, porque se configura sólo una vez y se puede distribuir todas las veces que sea necesario, a diferentes hosts y sin necesidad de reconfigurar una a una las librerías o el entorno del hardware:

El uso de contenedores facilita la distribución, pues el mismo contenedor puede ser enviado a múltiples equipos o servidores remotos
El uso de contenedores facilita la distribución, pues el mismo contenedor puede ser enviado a múltiples equipos o servidores remotos

Y esta idea de encapsular la aplicación y sus dependencias en un contenedor resulta súper útil en el Machine Learning Engineering, porque en un proyecto el equipo puede usar Docker para encapsular diferentes módulos del proyecto, lo que facilita no sólo la distribución a otros miembros del equipo sino que también reduce el tiempo de desarrollo, y facilita todo el proceso de despliegue y producción de la aplicación.

Y esto es mucho mejor que usar máquinas virtuales, pues por cada máquina virtual se requiere la instalación de un sistema operativo por cada aplicación que queramos ejecutar, mientras que con Docker cada aplicación estará en un contenedor, pero todos los contenedores estarán montados sobre un sólo sistema operativo. Esto hace que un contenedor necesite menos recursos, sea más liviano, mucho más portátil y se pueda ejecutar más rápido que una máquina virtual:

Comparación entre las máquinas virtuales (a la izquierda) y los contenedores Docker (a la derecha)
Comparación entre las máquinas virtuales (a la izquierda) y los contenedores Docker (a la derecha)

¿Cómo usar Docker en nuestros proyectos de Machine Learning?

Bien, teniendo ya una idea detallada de lo que es Docker, veamos una guía que les permitirá fácilmente comenzar a incorporarlo en sus proyectos de Machine Learning.

La creación de un contenedor en Docker es un proceso de tres fases:

Las fases de creación de un contenedor Docker
Las fases de creación de un contenedor Docker

Primero debemos crear un archivo Docker, que contiene simplemente un listado de instrucciones para empaquetar la aplicación. Este archivo se almacena en el directorio del proyecto bajo el nombre “Dockerfile”.

En segundo lugar está la imagen Docker, que es el paquete de software que contendrá el código, las librerías y todas las dependencias. Es lo que se distribuye a múltiples equipos remotos.

Y en tercer lugar está precisamente el contenedor Docker. El contenedor es simplemente el resultado de ejecutar la imagen en un equipo remoto. Es decir que la imagen es una sola, pero puede haber múltiples contenedores si la ejecutamos en múltiples hosts.

Veamos en detalle este flujo de trabajo.

El Dockerfile: un ejemplo detallado

Primero creamos el Dockerfile, que debe estar ubicado en la misma carpeta local del proyecto. Este archivo no tiene ninguna extensión y puede ser creado en un simple editor de texto:

El 'Dockerfile' debe estar ubicado en la misma carpeta local del proyecto
El 'Dockerfile' debe estar ubicado en la misma carpeta local del proyecto

El formato usado es una instrucción (en mayúscula), seguida de una serie de argumentos.

Usualmente la primera instrucción es FROM con la cual le indicaremos a Docker la imagen base sobre la cual construiremos el contenedor. Por ejemplo, si nuestra aplicación está desarrollada localmente sobre una versión de anaconda python, entonces debemos especificar esto en el argumento:

FROM continuumio/anaconda3

Otra instrucción muy usada es COPY con la cual le indicaremos a Docker que tome todo el contenido de la carpeta local del proyecto y lo copie en el directorio que especifiquemos para el contenedor:

FROM continuumio/anaconda3

# Copiar del directorio local (.) al directorio /usr/app del contenedor
COPY . /usr/app

Si por ejemplo nuestra aplicación tiene una interfaz a través del navegador, tendremos que habilitar un puerto de red. Para esto usamos la instrucción EXPOSE seguida del número del puerto de red que queremos usar:

FROM continuumio/anaconda3

# Copiar del directorio local (.) al directorio /usr/app del contenedor
COPY . /usr/app

# Habilitar puerto
EXPOSE 5000

Ahora debemos indicarle a Docker en qué directorio del contenedor se encontrará el archivo ejecutable de nuestra aplicación. Para esto usamos la instrucción WORKDIR seguida de la ruta completa del directorio en el contenedor:

FROM continuumio/anaconda3

# Copiar del directorio local (.) al directorio /usr/app del contenedor
COPY . /usr/app

# Habilitar puerto
EXPOSE 5000

# Directorio de trabajo del contenedor
WORKDIR /usr/app

Como nuestra aplicación muy probablemente requerirá varias librerías de Python, debemos crear un archivo de texto con estos requerimientos y luego, desde Dockerfile, incluir una instrucción para que estas librerías sean instaladas:

# requirements.txt

Flask==1.1.1
numpy>=1.9.2
scipy>=0.15.1
scikit-learn==0.22.1
matplotlib>=1.4.3
pandas>=0.19

Para instalar estos requerimientos usamos RUN seguido del comando para instalar las librerías:

FROM continuumio/anaconda3

# Copiar del directorio local (.) al directorio /usr/app del contenedor
COPY . /usr/app

# Habilitar puerto
EXPOSE 5000

# Directorio de trabajo del contenedor
WORKDIR /usr/app

# Instalar requerimientos
RUN pip install -r requirements.txt

Finalmente debemos especificar cuál comando se debe ejecutar al momento de correr la aplicación. Así que usamos CMD y, como nuestra aplicación está construida en Python, el comando será simplemente python seguido del nombre del archivo que contiene el ejecutable de la aplicación:

FROM continuumio/anaconda3

# Copiar del directorio local (.) al directorio /usr/app del contenedor
COPY . /usr/app

# Habilitar puerto
EXPOSE 5000

# Directorio de trabajo del contenedor
WORKDIR /usr/app

# Instalar requerimientos
RUN pip install -r requirements.txt

# Ejecución
CMD python app.py

Y listo, con esto ya tenemos el set de instrucciones, nuestro Dockerfile, con toda la información para poder crear la imagen y luego el contenedor.

Construcción de la imagen Docker

El siguiente paso es entonces construir la imagen. En este caso simplemente debemos abrir el terminal de Docker, que viene incluido en la aplicación de Docker instalada en nuestro computador local, nos movemos al directorio local en donde se encuentra nuestro Dockerfile, y escribimos el comando docker seguido de la palabra clave build, del nombre que queremos darle a la imagen y de la ruta en donde se encuentra el Dockerfile:

$ cd directorio_local
$ docker build -t mi_app .

Y listo, ya hemos creado la imagen en donde se ha encapsulado absolutamente todo: el código fuente, todas las dependencias y librerías requeridas para poder correr la aplicación remotamente.

Distribución de la aplicación

El último paso es distribuir esta aplicación para ejecutarla remotamente. Y aquí existen esencialmente tres alternativas:

La primera, que de hecho no es muy usada, consiste en crear un archivo comprimido de la imagen local (usando docker save), luego compartirlo con un usuario remoto y allí desempacarlo (con docker load) y luego ejecutarlo (con docker run):

Distribución del contenedor usando un archivo tar
Distribución del contenedor usando un archivo tar

La segunda es distribuirlo a través de Docker Hub, el servicio de Docker para compartir imágenes. Esto se logra simplemente construyendo la imagen (con docker build) y luego enviándola al repositorio usando docker push. Los usuarios remotos pueden descargar esta imagen con docker pull y luego ejecutarla con docker run:

Distribución de la imagen del contenedor usando un Docker Hub como puente entre el equipo local y el remoto
Distribución de la imagen del contenedor usando un Docker Hub como puente entre el equipo local y el remoto

Y finalmente está la opción más usada en Machine Learning Engineering, que consiste en crear la imagen local y luego distribuirla y también ejecutarla en un servicio en la nube, como Google Kubernetes, Amazon Elastic Container Service o Azure Kubernetes Service, entre otras. En este caso la imagen se crea nuevamente con docker build y se distribuye nuevamente docker push, pero la ejecución depende de las particularidades de cada uno de estos servicios:

Distribución de la imagen del contenedor a diferentes plataformas en la nube
Distribución de la imagen del contenedor a diferentes plataformas en la nube

Conclusión

En resumen, Docker es una herramienta que permite encapsular y distribuir fácilmente una aplicación de Machine Learning a través de lo que se conoce como un contenedor.

Esto resulta muy útil en diferentes fases de desarrollo de un proyecto de Machine Learning, pero especialmente cuando se quiere desplegar el modelo y llevarlo a producción. Así que si estás interesad@ en desempeñarse profesionalmente como Ingenier@ de Machine Learning, pues definitivamente Docker es una de esas herramientas que considero deberías conocer y saber manejar.

Como lo mencioné hace un momento, en un próximo artículo de esta serie de Machine Learning Engineering comenzaremos a hablar de plataformas en la nube, como las ofrecidas por Google, Amazon o Microsoft, que son tal vez de las más usadas en la actualidad para desplegar y poner en producción Modelos de Machine Learning.

Si tienes alguna duda de este artículo o tienes alguna sugerencia no dudes en contactarme diligenciando el siguiente formulario:

Debes introducir tu nombre.
Debes introducir tu apellido.
Debes introducir un email válido.
Debes introducir un mensaje.
Debes aceptar la política de privacidad.

Otros artículos que te pueden interesar