API de aprendizaje automático para la clasificación con FastAPI y transformadores

La primera versión de FastAPI fue lanzada a finales de 2018 y desde entonces se utiliza cada vez más en muchas aplicaciones en producción (ver el sitio web de FastAPI). Esto es lo que usamos detrás del capó en NLP Cloud. Es una gran manera de servir fácil y eficientemente a nuestros cientos de modelos de PLN, para la extracción de entidades (NER), la clasificación de textos, el análisis de preguntas, resúmenes... Descubrimos que FastAPI es una forma excelente de servir modelos de aprendizaje profundo basados en transformadores. basados en transformadores.

En este artículo, pensamos que sería interesante mostrarte cómo estamos implementando una API NLP basada en transformadores Hugging Face con FastAPI.

¿Por qué utilizar FastAPI?

Antes de FastAPI, habíamos utilizado esencialmente Django Rest Framework para nuestras APIs de Python, pero rápidamente nos interesados en FastAPI por las siguientes razones:

Estas grandes prestaciones hacen que FastAPI se adapte perfectamente a las API de aprendizaje automático que sirven modelos basados en transformadores como el nuestro.

Instalar FastAPI

Para que FastAPI funcione, lo estamos acoplando con el servidor ASGI de Uvicorn, que es la forma moderna de manejar de forma nativa las peticiones asíncronas de Python con asyncio. Usted puede decidir instalar FastAPI con Uvicorn manualmente o descargar una imagen Docker lista para usar. Vamos a mostrar la instalación manual primero:

pip install fastapi[all]

Entonces puedes empezar con:

uvicorn main:app

Sebastián Ramírez, el creador de FastAPI, proporciona varias imágenes Docker listas para usar que hacen muy fácil utilizar FastAPI en producción. La imagen Uvicorn + Gunicorn + FastAPI aprovecha las ventajas de Gunicorn para utilizar varios procesos en paralelo (ver la imagen aquí). Al final, gracias a Uvicorn puedes manejar varias instancias de FastAPI dentro del mismo proceso de Python, y gracias a Gunicorn puedes generar varios procesos Python.

Su aplicación FastAPI se iniciará automáticamente al arrancar el contenedor Docker con lo siguiente: docker run.

Es importante leer adecuadamente la documentación de estas imágenes Docker ya que hay algunos ajustes que puede querer ajustar, como por ejemplo el número de procesos paralelos creados por Gunicorn. Por defecto la imagen genera tantos procesos como el número de núcleos de la CPU en su máquina. Pero en el caso de modelos exigentes de de aprendizaje automático, como los Transformadores NLP, esto puede conducir rápidamente a decenas de GB de memoria utilizada. Una estrategia de estrategia sería aprovechar la opción Gunicorn (--preload), para cargar su modelo sólo una vez en memoria y compartirlo entre todos los procesos de FastAPI Python. Otra opción sería sería limitar el número de procesos de Gunicorn. Ambas opciones tienen ventajas e inconvenientes, pero eso está más allá del alcance de este artículo.

API simple FastAPI + Transformers para la clasificación de textos

La clasificación de textos es el proceso de determinar de qué habla un texto (¿Espacio? ¿De negocios? Alimentos?...). Más detalles sobre la clasificación aquí.

Queremos crear un endpoint de la API que realice la clasificación de texto utilizando el modelo Bart Large MNLI de Facebook de Facebook, que es un modelo preentrenado basado en los transformadores Hugging Face, perfectamente adecuado para la clasificación de texto. de texto.

Nuestro punto final de la API tomará un trozo de texto como entrada, junto con posibles categorías (llamadas etiquetas), y devolverá una puntuación para cada categoría (cuanto más alta, más probable).

Solicitaremos el endpoint con peticiones POST como esta:

curl "https://api.nlpcloud.io/v1/bart-large-mnli/classification" \
-H "Authorization: Token e7f6539e5a5d7a16e15" \
-X POST -d '{
    "text":"John Doe is a Go Developer at Google. He has been working there for 10 years and has been awarded employee of the year.",
    "labels":["job", "nature", "space"]
}'

Y a cambio obtendríamos una respuesta como:

{
    "labels": [
        "job",
        "space",
        "nature"
    ],
    "scores": [
        0.9258803129196167,
        0.19384843111038208,
        0.010988432914018631
    ]
}

Aquí se explica cómo conseguirlo con FastAPI y Transformers:

from fastapi import FastAPI
from pydantic import BaseModel, constr, conlist
from typing import List
from transformers import pipeline

classifier = pipeline("zero-shot-classification",
                model="facebook/bart-large-mnli")
app = FastAPI()

class UserRequestIn(BaseModel):
    text: constr(min_length=1)
    labels: conlist(str, min_items=1)

class ScoredLabelsOut(BaseModel):
    labels: List[str]
    scores: List[float]

@app.post("/classification", response_model=ScoredLabelsOut)
def read_classification(user_request_in: UserRequestIn):
    return classifier(user_request_in.text, user_request_in.labels)

Lo primero es lo primero: cargamos el MNLI Bart Large de Facebook desde el repositorio Hugging Face, y inicializarlo adecuadamente para fines de clasificación, gracias a Transformer Pipeline:

classifier = pipeline("zero-shot-classification",
                model="facebook/bart-large-mnli")

Y más tarde estamos utilizando el modelo haciendo esto:

classifier(user_request_in.text, user_request_in.labels)

Segunda cosa importante: estamos realizando una validación de datos gracias a Pydantic. Pydantic te obliga a declarar de antemano el formato de entrada y salida de su API, lo que es genial desde el punto de vista de la documentación documentación, pero también porque limita los posibles errores. En Go harías más o menos lo mismo con JSON unmarshalling con structs. Esta es una forma fácil de declarar que el campo "text" debe tener al menos 1 carácter: constr(min_length=1). Y lo siguiente especifica que la lista de entrada de etiquetas debe contener un elemento: conlist(str, min_items=1). Esta línea significa que el campo de salida "etiquetas" debe ser una lista de cadenas: List[str]. Y esta significa que las puntuaciones deben ser una lista de flotantes: List[float]. Si el modelo devuelve resultados que no siguen este formato, FastAPI emitirá automáticamente un error.

class UserRequestIn(BaseModel):
    text: constr(min_length=1)
    labels: conlist(str, min_items=1)

class ScoredLabelsOut(BaseModel):
    labels: List[str]
    scores: List[float]

Por último, el siguiente decorador facilita la especificación de que sólo se aceptan solicitudes POST, en un punto final específico: @app.post("/entities", response_model=EntitiesOut).

Validación de datos más avanzada

Puedes hacer cosas de validación más complejas, como por ejemplo la composición. Por ejemplo, digamos que estás haciendo Reconocimiento de Entidades Nombradas (NER), así que tu modelo está devolviendo una lista de entidades. Cada entidad tendría 4 campos: text, type, start y position. Así es como puedes hacerlo:

class EntityOut(BaseModel):
    start: int
    end: int
    type: str
    text: str

class EntitiesOut(BaseModel):
    entities: List[EntityOut]

@app.post("/entities", response_model=EntitiesOut) 
# [...]

Hasta ahora, hemos dejado que Pydantic se encargue de la validación. Funciona en la mayoría de los casos, pero a veces puede querer que quieras lanzar dinámicamente un error por ti mismo basado en condiciones complejas que no son manejadas nativamente por Pydantic. Por ejemplo, si quieres devolver manualmente un error HTTP 400, puedes hacer lo siguiente:

from fastapi import HTTPException

raise HTTPException(status_code=400, 
        detail="Your request is malformed")

Por supuesto, puedes hacer mucho más.

Establecer la ruta de acceso a la raíz

Si está utilizando FastAPI detrás de un proxy inverso, lo más probable es que tenga que jugar con la ruta raíz.

Lo difícil es que, detrás de un proxy inverso, la aplicación no conoce la ruta completa de la URL, así que tenemos que decirle explícitamente cuál es.

Por ejemplo, en este caso la URL completa de nuestro punto final podría no ser simplemente /classification. Pero podría ser algo como /api/v1/classification. No queremos codificar esta URL completa para que para que nuestro código de la API se acople libremente al resto de la aplicación. Podríamos hacer esto:

app = FastAPI(root_path="/api/v1")

O, alternativamente, podrías pasar un parámetro a Uvicorn al iniciarlo:

uvicorn main:app --root-path /api/v1

Conclusión:

Espero que hayamos mostrado con éxito lo conveniente que puede ser FastAPI para una API de NLP. Pydantic hace que el código muy expresivo y menos propenso a errores.

FastAPI tiene un gran rendimiento y hace posible el uso de Python asyncio fuera de la caja, que es grande para modelos de aprendizaje automático exigentes, como los modelos de NLP basados en Transformer. Hemos estado usando FastAPI durante casi 1 año en NLP Cloud y nunca nos ha decepcionado hasta ahora.

Si tienes alguna pregunta, no dudes en preguntar, ¡será un placer comentarlo!

Julien Salinas
CTO en NLP Cloud