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.
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.
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.
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)
.
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ù!
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
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