Diseño e implementación de una aplicación Python de aprendizaje automático (Parte 2) |  de Noah Haglund |  febrero de 2024

Como no hemos resuelto los problemas clave, profundicemos un poco más antes de entrar en el meollo de la cuestión de bajo nivel. Como lo afirma Heroku:

Las aplicaciones web que procesan solicitudes HTTP entrantes simultáneamente hacen un uso mucho más eficiente de los recursos dyno que las aplicaciones web que solo procesan una solicitud a la vez. Debido a esto, recomendamos utilizar servidores web que admitan el procesamiento de solicitudes simultáneas siempre que se desarrollen y ejecuten servicios de producción.

Los marcos web Django y Flask cuentan con convenientes servidores web integrados, pero estos servidores de bloqueo solo procesan una única solicitud a la vez. Si implementa con uno de estos servidores en Heroku, sus recursos de dinamómetro estarán infrautilizados y su aplicación no responderá.

Ya estamos a la vanguardia al utilizar el multiprocesamiento de trabajadores para la tarea de aprendizaje automático, pero podemos llevar esto un paso más allá usando Gunicorn:

gunicornio es un servidor HTTP puro de Python para aplicaciones WSGI. Le permite ejecutar cualquier aplicación Python simultáneamente ejecutando múltiples procesos Python dentro de un solo banco de pruebas. Proporciona un equilibrio perfecto entre rendimiento, flexibilidad y simplicidad de configuración.

Bien, genial, ahora podemos utilizar aún más procesos, pero hay un problema: cada nuevo proceso de trabajo de Gunicorn representará una copia de la aplicación, lo que significa que ellos también utilizarán la base de ~150 MB de RAM. además al proceso Heroku. Entonces, digamos que instalamos gunicorn y ahora inicializamos el proceso web de Heroku con el siguiente comando:

gunicorn <DJANGO_APP_NAME_HERE>.wsgi:application --workers=2 --bind=0.0.0.0:$PORT

La RAM base de ~150 MB en el proceso web se convierte en ~300 MB de RAM (uso de memoria base multiplicado por # trabajadores gunicorn).

Si bien somos cautelosos con las limitaciones de los subprocesos múltiples en una aplicación Python, también podemos agregar subprocesos a los trabajadores usando:

gunicorn <DJANGO_APP_NAME_HERE>.wsgi:application --threads=2 --worker-class=gthread --bind=0.0.0.0:$PORT

Incluso con el problema n.° 3, todavía podemos encontrar un uso para los subprocesos, ya que queremos asegurarnos de que nuestro proceso web sea capaz de procesar más de una solicitud a la vez y al mismo tiempo tener cuidado con el uso de memoria de la aplicación. Aquí, nuestros subprocesos podrían procesar solicitudes minúsculas y al mismo tiempo garantizar que la tarea de aprendizaje automático se distribuya en otros lugares.

De cualquier manera, al utilizar trabajadores gunicorn, subprocesos o ambos, estamos configurando nuestra aplicación Python para procesar más de una solicitud a la vez. Hemos resuelto más o menos el problema n.º 2 incorporando varias formas de implementar la concurrencia y/o el manejo de tareas paralelas, al mismo tiempo que garantizamos que la tarea crítica de aprendizaje automático de nuestra aplicación no dependa de posibles obstáculos, como el subproceso múltiple, lo que nos prepara para escalar y llegar a la raíz del problema n.° 3.

Bien, ¿qué pasa con ese complicado problema n.° 1? Al final del día, los procesos de ML normalmente terminarán sobrecargando el hardware de una forma u otra, ya sea memoria, CPU y/o GPU. Sin embargo, al utilizar un sistema distribuido, nuestra tarea de aprendizaje automático está integralmente vinculada al proceso web principal pero se maneja en paralelo a través de un trabajador de Celery. Podemos rastrear el inicio y el final de la tarea de ML a través del apio elegido. corredor, así como revisar métricas de forma más aislada. En este caso, limitar las configuraciones de los procesos de trabajo de Celery y Heroku depende de usted, pero es un excelente punto de partida para integrar un proceso de aprendizaje automático de larga duración y que consume mucha memoria en su aplicación.

Ahora que hemos tenido la oportunidad de profundizar realmente y obtener una imagen de alto nivel del sistema que estamos construyendo, armémoslo y centrémonos en los detalles.

Por su conveniencia, aquí está el repositorio Lo mencionaré en esta sección.

Primero comenzaremos configurando Django y Django Rest Framework, con guías de instalación. aquí y aquí respectivamente. Todos los requisitos para esta aplicación se pueden encontrar en el archivo requisitos.txt del repositorio (y Detectron2 y Torch se construirán a partir de ruedas de Python especificadas en el Dockerfile, para mantener pequeño el tamaño de la imagen de Docker).

La siguiente parte será configurar la aplicación Django, configurar el backend para guardar en AWS S3 y exponer un punto final usando DRF, por lo que si ya se siente cómodo haciendo esto, no dude en seguir adelante e ir directamente a la Configuración e implementación de tareas de aprendizaje automático sección.

Configuración de Django

Continúe y cree una carpeta para el proyecto Django y cd en ella. Active el entorno virtual/conda que está utilizando, asegúrese de que Detectron2 esté instalado según las instrucciones de instalación en Parte 1e instale los requisitos también.

Emita el siguiente comando en una terminal:

django-admin startproject mltutorial

Esto creará un directorio raíz del proyecto Django titulado “mltutorial”. Continúe y acceda a él para encontrar un archivo Manage.py y un subdirectorio mltutorial (que es el paquete Python real para su proyecto).

mltutorial/
manage.py
mltutorial/
__init__.py
settings.py
urls.py
asgi.py
wsgi.py

Abra settings.py y agregue ‘rest_framework’, ‘apio’ y ‘storages’ (necesarios para boto3/AWS) en la lista INSTALLED_APPS para registrar esos paquetes con el proyecto Django.

En el directorio raíz, creemos una aplicación que albergará la funcionalidad principal de nuestro backend. Emita otro comando de terminal:

python manage.py startapp docreader

Esto creará una aplicación en el directorio raíz llamada docreader.

También creemos un archivo en docreader titulado mltask.py. En él, defina una función simple para probar nuestra configuración que toma una variable, file_path, y la imprime:

def mltask(file_path):
return print(file_path)

Ahora, entrando en estructura, las aplicaciones Django usan el Controlador de vista de modelo (MVC) patrón de diseño, definiendo el Modelo en modelos.pyVer en vistas.pyy Controlador en Django Plantillas y URL.py. Usando Django Rest Framework, incluiremos la serialización en este proceso, que proporciona una forma de serializar y deserializar estructuras dativas nativas de Python en representaciones como json. Por tanto, la lógica de aplicación para exponer un punto final es la siguiente:

Base de datos ← → models.py ← → serializers.py ← → views.py ← → urls.py

En docreader/models.py, escriba lo siguiente:

from django.db import models
from django.dispatch import receiver
from .mltask import mltask
from django.db.models.signals import(
post_save
)

class Document(models.Model):
title = models.CharField(max_length=200)
file = models.FileField(blank=False, null=False)

@receiver(post_save, sender=Document)
def user_created_handler(sender, instance, *args, **kwargs):
mltask(str(instance.file.file))

Esto configura un documento modelo que requerirá un título y un archivo para cada entrada guardada en la base de datos. Una vez guardado, el decorador @receiver escucha una señal posterior al guardado, lo que significa que el modelo especificado, Documento, se guardó en la base de datos. Una vez guardado, user_created_handler() toma el campo de archivo de la instancia guardada y lo pasa a lo que se convertirá en nuestra función de aprendizaje automático.

Cada vez que se realicen cambios en models.py, deberá ejecutar los dos comandos siguientes:

python manage.py makemigrations
python manage.py migrate

En el futuro, cree un archivo serializers.py en docreader, que permita la serialización y deserialización de los campos de título y archivo del documento. Escribe en él:

from rest_framework import serializers
from .models import Document

class DocumentSerializer(serializers.ModelSerializer):
class Meta:
model = Document
fields = [
'title',
'file'
]

A continuación, en views.py, donde podemos definir nuestras operaciones CRUD, definamos la capacidad de crear, así como enumerar, entradas de documentos usando vistas genéricas (que esencialmente le permite escribir vistas rápidamente utilizando una abstracción de patrones de vista comunes):

from django.shortcuts import render
from rest_framework import generics
from .models import Document
from .serializers import DocumentSerializer

class DocumentListCreateAPIView(
generics.ListCreateAPIView):

queryset = Document.objects.all()
serializer_class = DocumentSerializer

Finalmente, actualice urls.py en mltutorial:

from django.contrib import admin
from django.urls import path, include

urlpatterns = [
path("admin/", admin.site.urls),
path('api/', include('docreader.urls')),
]

Y cree urls.py en el directorio de la aplicación docreader y escriba:

from django.urls import path

from . import views

urlpatterns = [
path('create/', views.DocumentListCreateAPIView.as_view(), name='document-list'),
]

Ahora estamos todos configurados para guardar una entrada de documento, con campos de título y campo, en el punto final /api/create/, que llamará a mltask() después de guardar. Entonces, probemos esto.

Para ayudar a visualizar las pruebas, registremos nuestro modelo de documento con Django. interfaz de administraciónpara que podamos ver cuando se ha creado una nueva entrada.

En docreader/admin.py escriba:

from django.contrib import admin
from .models import Document

admin.site.register(Document)

Cree un usuario que pueda iniciar sesión en la interfaz de administración de Django usando:

python manage.py createsuperuser

Ahora, probemos el punto final que expusimos.

Para hacer esto sin una interfaz, ejecute el servidor Django y vaya a Postman. Envíe la siguiente solicitud POST con un archivo PDF adjunto:

Si verificamos nuestros registros de Django, deberíamos ver la ruta del archivo impresa, como se especifica en la llamada a la función post save mltask().

Configuración de AWS

Notarás que el PDF se guardó en el directorio raíz del proyecto. Asegurémonos de que todos los medios se guarden en AWS S3, preparando nuestra aplicación para su implementación.

Ve a la consola s3 (y cree una cuenta y obtenga nuestra información de su cuenta Claves de acceso y secretas si aún no lo has hecho). Cree un nuevo depósito, aquí lo titularemos ‘djangomltest’. Actualice los permisos para garantizar que el depósito sea público para realizar pruebas (y revertirlo, según sea necesario, para producción).

Ahora, configuremos Django para que funcione con AWS.

Añade tu model_final.pth, entrenado en Parte 1, en el directorio docreader. Cree un archivo .env en el directorio raíz y escriba lo siguiente:

AWS_ACCESS_KEY_ID = <Add your Access Key Here>
AWS_SECRET_ACCESS_KEY = <Add your Secret Key Here>
AWS_STORAGE_BUCKET_NAME = 'djangomltest'

MODEL_PATH = './docreader/model_final.pth'

Actualice settings.py para incluir configuraciones de AWS:

import os
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())

# AWS
AWS_ACCESS_KEY_ID = os.environ['AWS_ACCESS_KEY_ID']
AWS_SECRET_ACCESS_KEY = os.environ['AWS_SECRET_ACCESS_KEY']
AWS_STORAGE_BUCKET_NAME = os.environ['AWS_STORAGE_BUCKET_NAME']

#AWS Config
AWS_DEFAULT_ACL = 'public-read'
AWS_S3_CUSTOM_DOMAIN = f'{AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com'
AWS_S3_OBJECT_PARAMETERS = {'CacheControl': 'max-age=86400'}

#Boto3
STATICFILES_STORAGE = 'mltutorial.storage_backends.StaticStorage'
DEFAULT_FILE_STORAGE = 'mltutorial.storage_backends.PublicMediaStorage'

#AWS URLs
STATIC_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/static/'
MEDIA_URL = f'https://{AWS_S3_CUSTOM_DOMAIN}/media/'

Opcionalmente, con AWS sirviendo nuestros archivos estáticos y multimedia, querrá ejecutar el siguiente comando para entregar activos estáticos a la interfaz de administración usando S3:

python manage.py collectstatic

Si ejecutamos el servidor nuevamente, nuestro administrador debería aparecer igual que con nuestros archivos estáticos servidos localmente.

Una vez más, ejecutemos el servidor Django y probemos el punto final para asegurarnos de que el archivo ahora esté guardado en S3.

Configuración e implementación de tareas de aprendizaje automático

Con Django y AWS configurados correctamente, configuremos nuestro proceso de ML en mltask.py. Como el archivo es largo, consulte el repositorio. aquí como referencia (con comentarios agregados para ayudar a comprender los distintos bloques de código).

Lo que es importante ver es que Detectron2 se importa y el modelo se carga solo cuando se llama a la función. Aquí, llamaremos a la función solo a través de una tarea de Celery, asegurando que la memoria utilizada durante la inferencia esté aislada del proceso de trabajo de Heroku.

Finalmente, configuremos Celery y luego implementemos en Heroku.

En mltutorial/_init__.py escribe:

from .celery import app as celery_app
__all__ = ('celery_app',)

Cree celery.py en el directorio mltutorial y escriba:

import os

from celery import Celery

# Set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mltutorial.settings')

# We will specify Broker_URL on Heroku
app = Celery('mltutorial', broker=os.environ['CLOUDAMQP_URL'])

# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')

# Load task modules from all registered Django apps.
app.autodiscover_tasks()

@app.task(bind=True, ignore_result=True)
def debug_task(self):
print(f'Request: {self.request!r}')

Por último, crea un task.py en docreader y escribe:

from celery import shared_task
from .mltask import mltask

@shared_task
def ml_celery_task(file_path):
mltask(file_path)
return "DONE"

Esta tarea de Apio, ml_celery_task(), ahora debe importarse a models.py y usarse con la señal posterior a guardar en lugar de la función mltask extraída directamente de mltask.py. Actualice el bloque de señal post_save a lo siguiente:

@receiver(post_save, sender=Document)
def user_created_handler(sender, instance, *args, **kwargs):
ml_celery_task.delay(str(instance.file.file))

Y para probar el apio, ¡implementemos!

En el directorio raíz del proyecto, incluya un archivo Dockerfile y heroku.yml, ambos especificados en el repositorio. Lo más importante es editar heroku.yml. comandos le permitirá configurar el proceso web gunicorn y el proceso de trabajo Celery, lo que puede ayudar a mitigar aún más problemas potenciales.

Cree una cuenta Heroku y cree una nueva aplicación llamada “mlapp” y ignore el archivo .env. Luego inicialice git en el directorio raíz de proyectos y cambie la pila de la aplicación Heroku a contenedor (para implementarla usando Docker):

$ heroku login
$ git init
$ heroku git:remote -a mlapp
$ git add .
$ git commit -m "initial heroku commit"
$ heroku stack:set container
$ git push heroku master

Una vez presionado, solo necesitamos agregar nuestras variables env a la aplicación Heroku.

Vaya a configuración en la interfaz en línea, desplácese hacia abajo hasta Config Vars, haga clic en Revelar Config Vars y agregue cada línea enumerada en el archivo .env.

Es posible que hayas notado que había una variable CLOUDAMQP_URL especificada en celery.py. Necesitamos aprovisionar un Celery Broker en Heroku, para el cual hay una variedad de opciones. estaré usando NubeAMQP que tiene un nivel gratuito. Continúe y agregue esto a su aplicación. Una vez agregada, la variable de entorno CLOUDAMQP_URL se incluirá automáticamente en Config Vars.

Finalmente, probemos el producto final.

Para monitorear solicitudes, ejecute:

$ heroku logs --tail

Emita otra solicitud POST de Postman a la URL de la aplicación Heroku en el punto final /api/create/. Verá llegar la solicitud POST, Celery recibirá la tarea, cargará el modelo y comenzará a ejecutar las páginas:

Continuaremos viendo “Ejecutando la página …” hasta el final del proceso y podrá verificar el depósito de AWS S3 mientras se ejecuta.

¡Felicitaciones! ¡Ahora ha implementado y ejecutado un backend de Python utilizando Machine Learning como parte de una cola de tareas distribuida que se ejecuta en paralelo al proceso web principal!

Como se mencionó, querrás ajustar heroku.yml comandos para incorporar hilos de gunicornio y/o procesos de trabajo y afinar el apio. Para obtener más información, aquí hay un Excelente artículo sobre la configuración de gunicorn para satisfacer las necesidades de su aplicación, una para profundizar en Apio para producción.y otro para explorar el apio. grupos de trabajadorespara ayudarle a gestionar adecuadamente sus recursos.

¡Feliz codificación!

A menos que se indique lo contrario, todas las imágenes utilizadas en este artículo son del autor.