Den første version af FastAPI er blevet frigivet i slutningen af 2018, og den er siden da blevet anvendt i stigende grad i mange applikationer i produktion (se FastAPI's websted). Det er det, vi bruger bag kølerhjelmen i NLP Cloud. Det er en fantastisk måde at betjene vores hundredvis af Natural Language Processing-modeller til udtrækning af enheder (NER), tekstklassificering, sentimentanalyse, spørgsmål besvarelse, opsummering ... Vi fandt ud af, at FastAPI er en fantastisk måde at tjene transformerbaserede dybe læringsmodeller.
I denne artikel tænkte vi, at det ville være interessant at vise dig, hvordan vi implementerer en API til behandling af naturligt sprog baseret på på Hugging Face-transformere med FastAPI.
Før FastAPI havde vi hovedsageligt brugt Django Rest Framework til vores Python API'er, men vi blev hurtigt interesseret i FastAPI af følgende grunde:
Disse fantastiske præstationer gør FastAPI perfekt egnet til API'er til maskinlæring, der tjener transformerbaserede modeller som vores.
For at FastAPI kan fungere, kobler vi det med Uvicorn ASGI-serveren, som er den moderne måde at nativt at håndtere asynkrone Python-forespørgsler med asyncio. Du kan enten vælge at installere FastAPI med Uvicorn manuelt eller downloade et færdigt Docker-image. Lad os vise den manuelle installation først:
pip install fastapi[all]
Så kan du starte den med:
uvicorn main:app
Sebastián Ramírez, skaberen af FastAPI, leverer flere færdige Docker-aftryk, der gør det meget nemt at bruge nemt at bruge FastAPI i produktionen. Uvicorn + Gunicorn + FastAPI image udnytter Gunicorn til at bruge flere processer parallelt (se billedet her). I sidste ende, takket være Uvicorn kan du håndtere flere FastAPI-instanser i den samme Python-proces, og takket være Gunicorn kan du spawne flere Python-processer.
Din FastAPI-applikation starter automatisk, når du starter Docker-containeren med følgende:
docker run.
Det er vigtigt at læse dokumentationen for disse Docker-aftryk ordentligt, da der er nogle indstillinger, som du
måske ønsker at justere, som f.eks. antallet af parallelle processer, der oprettes af Gunicorn. Som standard,
spawner afbilledet lige så mange processer som antallet af CPU-kerner på din maskine. Men i tilfælde af krævende
maskinlæringsmodeller som Natural Language Processing Transformers kan det hurtigt føre til, at der bruges titusindvis af GB hukommelse. En
strategi ville være at udnytte Gunicorn-indstillingen (--preload), for at indlæse
din model kun én gang i hukommelsen og dele den mellem alle FastAPI Python-processerne. En anden mulighed ville være
være at begrænse antallet af Gunicorn-processer. Begge har fordele og ulemper, men det ligger uden for
rækkevidde af denne artikel.
Tekstklassificering er en proces, hvor det bestemmes, hvad en tekst handler om (Rum? Erhverv? Mad?...). Flere oplysninger om tekst klassificering her.
Vi ønsker at oprette et API endpoint, der udfører tekstklassificering ved hjælp af Facebooks Bart Large MNLI model, som er en forudtrænet model baseret på Hugging Face-transformere, der er perfekt egnet til tekst klassifikation.
Vores API-slutpunkt vil tage et stykke tekst som input sammen med potentielle kategorier (kaldet labels), og returnerer en score for hver kategori (jo højere, jo mere sandsynligt).
Vi anmoder om slutpunktet med POST-forespørgsler på denne måde:
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"]
}'
Og til gengæld fik vi et svar som:
{
"labels": [
"job",
"space",
"nature"
],
"scores": [
0.9258803129196167,
0.19384843111038208,
0.010988432914018631
]
}
Her er hvordan du opnår det med FastAPI og 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)
Først og fremmest: Vi indlæser Facebook's Bart Large MNLI fra Hugging Face-repositoriet, og initialiserer den korrekt med henblik på klassificering takket være Transformer Pipeline:
classifier = pipeline("zero-shot-classification",
model="facebook/bart-large-mnli")
Og senere bruger vi modellen ved at gøre dette:
classifier(user_request_in.text, user_request_in.labels)
Den anden vigtige ting: vi udfører datavalidering takket være Pydantic. Pydantic tvinger dig til at
på forhånd at deklarere input- og outputformatet for dit API, hvilket er fantastisk ud fra en dokumentation
synspunkt, men også fordi det begrænser potentielle fejl. I Go ville du gøre stort set det samme
med JSON unmarshalling med structs. Her er en nem måde at
erklære, at feltet "text" skal mindst have 1 tegn: constr(min_length=1).
Og følgende angiver, at den indtastede liste over etiketter skal indeholde ét element i listen:
conlist(str,
min_items=1).
Denne linje betyder, at outputfeltet "labels" skal være en liste over strenge: List[str].
Og denne betyder, at scorerne skal være en liste af floats: List[float].
Hvis modellen returnerer resultater, der ikke følger dette format, vil FastAPI automatisk give en fejlmeddelelse.
class UserRequestIn(BaseModel):
text: constr(min_length=1)
labels: conlist(str, min_items=1)
class ScoredLabelsOut(BaseModel):
labels: List[str]
scores: List[float]
Sidst men ikke mindst gør følgende dekorator det nemt at angive, at du kun accepterer POST-forespørgsler på et bestemt slutpunkt:
@app.post("/entities", response_model=EntitiesOut).
Du kan lave mange mere komplekse valideringsting, f.eks. sammensætning. Lad os f.eks. sige, at
du laver Named Entity Recognition (NER), så din model returnerer en liste over enheder. Hver enhed
ville have 4 felter: text, type, start og position. Sådan kan du gøre det:
class EntityOut(BaseModel):
start: int
end: int
type: str
text: str
class EntitiesOut(BaseModel):
entities: List[EntityOut]
@app.post("/entities", response_model=EntitiesOut)
# [...]
Indtil nu har vi ladet Pydantic stå for valideringen. Det fungerer i de fleste tilfælde, men nogle gange vil man måske ønske selv dynamisk udløse en fejl baseret på komplekse betingelser, som ikke håndteres nativt af Pydantic. Hvis du f.eks. ønsker at returnere en HTTP 400-fejl manuelt, kan du gøre følgende:
from fastapi import HTTPException
raise HTTPException(status_code=400,
detail="Your request is malformed")
Selvfølgelig kan du gøre meget mere!
Hvis du bruger FastAPI bag en reverse proxy, skal du sandsynligvis lege med rodstien.
Det svære er, at programmet bag en reverse proxy ikke kender hele URL-stien, så vi er nødt til udtrykkeligt at fortælle den, hvilken det er.
Her er den fulde URL til vores slutpunkt f.eks. ikke nødvendigvis blot /classification. Men det kunne være noget i retning af /api/v1/classification. Vi ønsker ikke at hardcode denne fulde URL for at
vores API-kode skal være løst koblet med resten af programmet. Vi kunne gøre dette:
app = FastAPI(root_path="/api/v1")
Alternativt kan du også sende en parameter til Uvicorn, når du starter den:
uvicorn main:app --root-path /api/v1
Jeg håber, at det lykkedes os at vise dig, hvor praktisk FastAPI kan være til en API til behandling af naturligt sprog. Pydantic gør koden meget udtryksfuld og mindre fejlbehæftet.
FastAPI har en fantastisk ydeevne og gør det muligt at bruge Python asyncio out of the box, hvilket er fantastisk til krævende maskinlæringsmodeller som Transformer-baserede Natural Language Processing-modeller. Vi har brugt FastAPI til næsten 1 år hos NLP Cloud, og vi har aldrig været skuffede indtil videre.
Hvis du har spørgsmål, så tøv ikke med at spørge, det vil være en fornøjelse at kommentere!
Julien Salinas
Teknisk direktør hos NLP Cloud