Produktionsdugligt API för maskininlärning och behandling av naturligt språk för klassificering med FastAPI och transformatorer

Den första versionen av FastAPI har släppts. i slutet av 2018 och den har använts i allt större utsträckning i många produktionsapplikationer sedan dess. (se FastAPI:s webbplats). Det är Det är vad vi använder bakom huven på NLP Cloud. Det är ett utmärkt sätt att enkelt och effektivt betjäna våra hundratals modeller för behandling av naturligt språk, för utvinning av enheter (NER), textklassificering, sentimentanalys, frågeställningar och svar, sammanfattning... Vi fann att FastAPI är ett utmärkt sätt att tjäna transformatorbaserade djupa inlärningsmodeller.

I den här artikeln tänkte vi att det skulle vara intressant att visa hur vi implementerar ett API för bearbetning av naturligt språk baserat på på Hugging Face-transformatorer med FastAPI.

Varför använda FastAPI?

Innan FastAPI hade vi i huvudsak använt Django Rest Framework för våra Python-API:er, men vi blev snabbt snabbt intresserade av FastAPI av följande skäl:

Dessa fantastiska prestanda gör FastAPI perfekt lämpad för API:er för maskininlärning som betjänar transformatorbaserade modeller som vår.

Installera FastAPI

För att FastAPI ska fungera kopplar vi den till Uvicorns ASGI-server, vilket är det moderna sättet att nativt hantera asynkrona Python-förfrågningar med asyncio. Du kan antingen välja att installera FastAPI med Uvicorn manuellt eller ladda ner en färdig Docker-avbildning. Låt oss visa den manuella installationen först:

pip install fastapi[all]

Då kan du börja med:

uvicorn main:app

Sebastián Ramírez, skaparen av FastAPI, tillhandahåller flera färdiga Docker-avbildningar som gör det mycket enkelt att att använda FastAPI i produktionen. Uvicorn + Gunicorn + FastAPI använder Gunicorn för att använda flera processer parallellt. (se bilden här). I slutändan, tack vare Uvicorn kan du hantera flera FastAPI-instanser inom samma Python-process, och tack vare Gunicorn kan du skapa flera Pythonprocesser.

Din FastAPI-applikation startar automatiskt när du startar Docker-behållaren med följande: docker run.

Det är viktigt att läsa dokumentationen för dessa Docker-avbildningar ordentligt eftersom det finns vissa inställningar som du vill justera, som till exempel antalet parallella processer som skapas av Gunicorn. Som standard, avbildningen skapar lika många processer som antalet CPU-kärnor på din maskin. Men vid krävande maskininlärningsmodeller som Natural Language Processing Transformers kan det snabbt leda till att tiotals GB minnet används. En strategi skulle vara att utnyttja Gunicorn-alternativet. (--preload), för att ladda din modell endast en gång i minnet och dela den mellan alla FastAPI Python-processer. Ett annat alternativ är att begränsa antalet Gunicorn-processer. Båda har för- och nackdelar, men det ligger utanför räckvidd för den här artikeln.

Enkel FastAPI + Transformers API för textklassificering

Klassificering av texter är processen att bestämma vad en text talar om (Space? Affärer? Mat?...). Mer information om text klassificering här.

Vi vill skapa en API-slutpunkt som utför textklassificering med hjälp av Facebooks Bart Large MNLI. modell, som är en förtränad modell baserad på Hugging Face-transformatorer, som är perfekt lämpad för textklassificering. klassificering.

Vår API-slutpunkt tar emot ett stycke text som indata tillsammans med potentiella kategorier (kallade etiketter), och den returnerar en poäng för varje kategori (ju högre, desto mer troligt).

Vi begär slutpunkten med POST-förfrågningar på följande sätt:

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

Och i gengäld fick vi ett svar som:

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

Så här gör du med FastAPI och 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 och främst laddar vi Facebooks Bart Large MNLI från Hugging Face-förrådet, och och initialiserar den korrekt för klassificering, tack vare Transformer Pipeline:

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

Senare använder vi modellen genom att göra detta:

classifier(user_request_in.text, user_request_in.labels)

Den andra viktiga saken är att vi utför datavalidering tack vare Pydantic. Pydantic tvingar dig att att i förväg deklarera in- och utdataformatet för ditt API, vilket är bra ur dokumentationssynpunkt. men också för att det begränsar potentiella misstag. I Go skulle du göra i stort sett samma sak med JSON unmarshalling med structs. Här är ett enkelt sätt att deklarera att fältet "text" ska ha minst 1 tecken: constr(min_length=1). Följande anger att listan med etiketter i inmatningen ska innehålla ett element: conlist(str, min_items=1). Den här raden innebär att utdatafältet "labels" ska vara en lista med strängar: List[str]. Och den här innebär att poängen ska vara en lista med flotters: List[float]. Om modellen returnerar resultat som inte följer detta format kommer FastAPI automatiskt att ge ett fel.

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

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

Slutligen gör följande dekorator det enkelt att ange att du endast accepterar POST-begäranden på en specifik slutpunkt: @app.post("/entities", response_model=EntitiesOut).

Mer avancerad datavalidering

Du kan göra många mer komplexa valideringsfunktioner, som till exempel komposition. Låt oss till exempel säga att du gör Named Entity Recognition (NER), så din modell returnerar en lista med enheter. Varje enhet skulle ha fyra fält: text, type, start och position. Så här kan du göra:

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

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

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

Hittills har vi låtit Pydantic sköta valideringen. Det fungerar i de flesta fall, men ibland kan man vilja ha dynamiskt utlösa ett fel själv baserat på komplexa förhållanden som inte hanteras av Pydanyds Pydantic. Om du till exempel vill returnera ett HTTP 400-fel manuellt kan du göra följande:

from fastapi import HTTPException

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

Naturligtvis kan du göra mycket mer!

Ställa in rotvägen

Om du använder FastAPI bakom en omvänd proxy måste du troligen ändra rotvägen.

Det svåra är att programmet bakom en omvänd proxy inte känner till hela URL-sökvägen, så vi måste uttryckligen tala om vilken det är.

Till exempel kan den fullständiga URL:en till vår slutpunkt inte bara vara /classification. Men det skulle kunna vara något i stil med /api/v1/classification. Vi vill inte hårdkoda den fullständiga URL:n för att vår API-kod ska vara löst kopplad till resten av programmet. Vi kan göra så här:

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

Alternativt kan du skicka en parameter till Uvicorn när du startar den:

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

Slutsats

Jag hoppas att vi lyckades visa dig hur praktiskt FastAPI kan vara för ett API för behandling av naturligt språk. Pydantic gör koden mycket uttrycksfull och mindre felbenägen.

FastAPI har bra prestanda och gör det möjligt att använda Python asyncio direkt, vilket är bra. för krävande maskininlärningsmodeller som Transformer-baserade modeller för behandling av naturligt språk. Vi har använt FastAPI för nästan 1 år på NLP Cloud och vi har aldrig blivit besvikna hittills.

Om du har några frågor, tveka inte att fråga, det är ett nöje att kommentera!

Julien Salinas
CTO på NLP Cloud