API di elaborazione del linguaggio naturale per l'apprendimento automatico pronto per la produzione per la classificazione con FastAPI e trasformatori

La prima versione di FastAPI è stata rilasciata alla fine del 2018 e da allora è stata sempre più utilizzata in molte applicazioni in produzione (vedere il sito web di FastAPI). Questo è È quello che stiamo usando dietro il cofano di NLP Cloud. È un ottimo modo per servire in modo semplice ed efficiente le nostre centinaia di modelli di elaborazione del linguaggio naturale, per l'estrazione di entità (NER), classificazione del testo, analisi del sentimento, risposta alle domande risposta alle domande, riassunto... Abbiamo scoperto che FastAPI è un ottimo modo per servire modelli di deep modelli di apprendimento profondo basati su trasformatori.

In questo articolo, abbiamo pensato che sarebbe stato interessante mostrarvi come stiamo implementando un'API di elaborazione del linguaggio naturale basata su trasformatori Hugging Face con FastAPI.

Perché usare FastAPI?

Prima di FastAPI, avevamo essenzialmente usato Django Rest Framework per le nostre API Python, ma siamo stati subito interessati a FastAPI per le seguenti ragioni:

Queste grandi prestazioni rendono FastAPI perfettamente adatto alle API di apprendimento automatico che servono modelli basati su trasformatori come il nostro.

Installare FastAPI

Affinché FastAPI funzioni, lo stiamo accoppiando con il server ASGI di Uvicorn, che è il modo moderno di gestire nativamente le richieste Python asincrone con asyncio. Potete decidere di installare FastAPI con Uvicorn manualmente o scaricare un'immagine Docker pronta all'uso. Mostriamo prima l installazione manuale prima:

pip install fastapi[all]

Poi si può iniziare con:

uvicorn main:app

Sebastián Ramírez, il creatore di FastAPI, fornisce diverse immagini Docker pronte all'uso che rendono molto facile usare FastAPI in produzione. L'immagine Uvicorn + Gunicorn + FastAPI sfrutta Gunicorn per utilizzare diversi processi in parallelo (vedere l'immagine qui). Alla fine, grazie a Uvicorn potete gestire diverse istanze FastAPI all'interno dello stesso processo Python, e grazie a Gunicorn è possibile generare diversi processi Python.

La tua applicazione FastAPI si avvierà automaticamente all'avvio del contenitore Docker con il seguente: docker run.

È importante leggere correttamente la documentazione di queste immagini Docker, poiché ci sono alcune impostazioni che si come ad esempio il numero di processi paralleli creati da Gunicorn. Per impostazione predefinita, l'immagine genera tanti processi quanti sono i core della CPU sulla vostra macchina. Ma nel caso di modelli di modelli di apprendimento automatico come Natural Language Processing Transformers, può portare rapidamente a decine di GB di memoria utilizzata. Una strategia potrebbe essere quella di sfruttare l'opzione Gunicorn (--preload), al fine di caricare il vostro modello solo una volta in memoria e condividerlo tra tutti i processi FastAPI Python. Un'altra opzione potrebbe essere quella di limitare il numero di processi Gunicorn. Entrambi hanno vantaggi e svantaggi, ma questo va oltre lo scopo di questo articolo.

Semplice FastAPI + Transformers API per la classificazione del testo

La classificazione del testo è il processo di determinare di cosa parla un pezzo di testo (Spazio? Affari? Cibo?...). Maggiori dettagli sul testo classificazione qui.

Vogliamo creare un endpoint API che esegua la classificazione del testo utilizzando il modello Bart Large MNLI di Facebook che è un modello pre-addestrato basato su trasformatori Hugging Face, perfettamente adatto alla classificazione del testo.

Il nostro endpoint API prenderà un pezzo di testo come input, insieme a potenziali categorie (chiamate etichette), e restituirà un punteggio per ogni categoria (più alto, più probabile).

Richiederemo l'endpoint con richieste POST come questa:

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"]
}'

E in cambio avremmo una risposta come:

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

Ecco come ottenerlo con FastAPI e 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)

Prima di tutto: stiamo caricando il Bart Large MNLI di Facebook dal repository di Hugging Face, e inizializzandolo correttamente ai fini della classificazione, grazie a Transformer Pipeline:

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

E più tardi useremo il modello facendo questo:

classifier(user_request_in.text, user_request_in.labels)

Seconda cosa importante: stiamo eseguendo la validazione dei dati grazie a Pydantic. Pydantic vi costringe a dichiarare in anticipo il formato di input e di output per la vostra API, il che è ottimo dal punto di vista della documentazione ma anche perché limita i potenziali errori. In Go fareste più o meno la stessa cosa con l'unmarshalling JSON con le strutture. Ecco un modo semplice per dichiarare che il campo "text" deve avere almeno 1 carattere: constr(min_length=1). E la seguente specifica che la lista di etichette in ingresso deve contenere un elemento: conlist(str, min_items=1). Questa linea significa che il campo di output "labels" dovrebbe essere una lista di stringhe: List[str]. E questo significa che i punteggi dovrebbero essere una lista di float: List[float]. Se il modello restituisce risultati che non seguono questo formato, FastAPI solleverà automaticamente un errore.

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

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

Infine, il seguente decoratore rende facile specificare che si accettano solo richieste POST, su un endpoint specifico: @app.post("/entities", response_model=EntitiesOut).

Convalida dei dati più avanzata

Si possono fare molte cose di convalida più complesse, come per esempio la composizione. Per esempio, diciamo che stai facendo il Named Entity Recognition (NER), quindi il tuo modello sta restituendo una lista di entità. Ogni entità avrebbe 4 campi: text, type, start e position. Ecco come potreste farlo:

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

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

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

Fino ad ora, abbiamo lasciato che Pydantic gestisse la convalida. Funziona nella maggior parte dei casi, ma a volte si potrebbe voler sollevare dinamicamente un errore da soli in base a condizioni complesse che non sono gestite nativamente da Pydantic. Per esempio, se volete restituire manualmente un errore HTTP 400, potete fare come segue:

from fastapi import HTTPException

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

Naturalmente si può fare molto di più!

Impostare il percorso radice

Se state usando FastAPI dietro un reverse proxy, molto probabilmente avrete bisogno di giocare con il percorso di root.

La cosa difficile è che, dietro un reverse proxy, l'applicazione non conosce l'intero percorso dell'URL, quindi dobbiamo dirgli esplicitamente qual è.

Per esempio qui l'URL completo del nostro endpoint potrebbe non essere semplicemente /classification. Ma potrebbe essere qualcosa come /api/v1/classification. Non vogliamo codificare questo URL completo in modo che il nostro codice API sia liberamente accoppiato con il resto dell'applicazione. Potremmo fare così:

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

O in alternativa si potrebbe passare un parametro a Uvicorn quando lo si avvia:

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

Conclusione

Spero di avervi mostrato con successo quanto FastAPI possa essere conveniente per un'API di elaborazione del linguaggio naturale. Pydantic rende il codice molto espressivo e meno soggetto ad errori.

FastAPI ha grandi prestazioni e rende possibile l'uso di Python asyncio out of the box, che è ottimo per modelli di apprendimento automatico esigenti come i modelli di elaborazione del linguaggio naturale basati su Transformer. Abbiamo usato FastAPI per quasi 1 anno a NLP Cloud e non siamo mai stati delusi finora.

Per qualsiasi domanda, non esitate a chiedere, sarà un piacere commentare!

Julien Salinas
CTO di NLP Cloud