Zmagasz się z AI lub rozwojem full-stack? Nasi eksperci są tutaj, aby Cię poprowadzić: dostosowane porady, integracja techniczna i nie tylko. Skontaktuj się z nami pod adresem [email protected].

Gotowy do produkcji interfejs API do przetwarzania języka naturalnego w procesie uczenia maszynowego dla klasyfikacji z FastAPI i transformatorami.

Pierwsza wersja FastAPI została wydana pod koniec 2018 roku i od tego czasu jest coraz częściej wykorzystywana w wielu aplikacjach w produkcji (zobacz stronę FastAPI). To jest to, czego używamy za maską w NLP Cloud. Jest to świetny sposób, aby łatwo i efektywnie obsługiwać nasze setki modeli przetwarzania języka naturalnego, do ekstrakcji encji (NER), klasyfikacji tekstu, analizy sentymentu, odpowiedzi na pytania, podsumowania... odpowiadanie na pytania, streszczanie... Stwierdziliśmy, że FastAPI jest świetnym sposobem na obsługę opartych na transformatorach modele uczenia głębokiego.

W tym artykule, pomyśleliśmy, że będzie interesujące pokazać, jak wdrażamy API do przetwarzania języka naturalnego oparte na transformatorach Hugging Face za pomocą FastAPI.

Dlaczego używać FastAPI?

Przed FastAPI, zasadniczo używaliśmy Django Rest Framework dla naszych API Pythona, ale szybko zainteresowaliśmy się zainteresowani FastAPI z następujących powodów:

Te wspaniałe osiągi sprawiają, że FastAPI doskonale nadaje się do API uczenia maszynowego obsługującego modele oparte na transformatorach, takie jak nasze.

Zainstaluj FastAPI

Aby FastAPI mogło działać, sprzęgamy je z serwerem ASGI Uvicorn, który jest nowoczesnym sposobem na natywnie obsługiwać asynchroniczne żądania Pythona za pomocą asyncio. Możesz albo zdecydować się na zainstalować FastAPI z Uvicornem ręcznie lub pobrać gotowy do użycia obraz Dockera. Zaprezentujmy najpierw instalację ręczną:

pip install fastapi[all]

Wtedy możesz zacząć od:

uvicorn main:app

Sebastián Ramírez, twórca FastAPI, udostępnia kilka gotowych do użycia obrazów Dockera, które bardzo ułatwiają łatwe do wykorzystania FastAPI w produkcji. Obraz Uvicorn + Gunicorn + FastAPI wykorzystuje Gunicorn, aby używać kilku procesów równolegle (zobacz zdjęcie tutaj). W końcu, dzięki Uvicorn możesz obsługiwać kilka instancji FastAPI w ramach jednego procesu Pythona, a dzięki Gunicorn można wywoływać kilka procesów Pythona.

Twoja aplikacja FastAPI zostanie automatycznie uruchomiona podczas uruchamiania kontenera Docker z następującymi parametrami: docker run.

Ważne jest, aby prawidłowo przeczytać dokumentację tych obrazów Dockera, ponieważ istnieją pewne ustawienia, które możesz jak na przykład liczba równoległych procesów tworzonych przez Gunicorn. Domyślnie, obraz tworzy tyle procesów, ile jest rdzeni procesora na twojej maszynie. Ale w przypadku wymagających modeli uczenia maszynowego, takich jak Transformatory Przetwarzania Języka Naturalnego, może to szybko doprowadzić do wykorzystania dziesiątek GB pamięci. Jedną z strategią jest wykorzystanie opcji Gunicorn (--preload), w celu załadowania model tylko raz do pamięci i podzielić go pomiędzy wszystkie procesy FastAPI Pythona. Inną opcją jest ograniczenie liczby procesów Gunicorn. Oba rozwiązania mają zalety i wady, ale to wykracza poza zakres tego artykułu.

Proste API FastAPI + Transformers dla klasyfikacji tekstu

Klasyfikacja tekstu to proces określania, o czym mówi dany fragment tekstu (Przestrzeń? Biznes? Żywność?...). Więcej szczegółów na temat tekstu klasyfikacja tutaj.

Chcemy stworzyć punkt końcowy API, który będzie wykonywał klasyfikację tekstu przy użyciu modelu Bart Large MNLI z Facebooka. który jest wstępnie wytrenowanym modelem opartym na transformatach Hugging Face, idealnie nadającym się do klasyfikacji tekstu. klasyfikacji.

Nasz punkt końcowy API będzie pobierał fragment tekstu jako dane wejściowe, wraz z potencjalnymi kategoriami (zwanymi etykietami), i zwróci wynik dla każdej kategorii (im wyższy, tym bardziej prawdopodobne).

Będziemy żądać punktu końcowego za pomocą żądania POST, jak poniżej:

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

A w zamian otrzymalibyśmy odpowiedź w rodzaju:

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

Oto jak to osiągnąć za pomocą FastAPI i Transformerów:

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)

Po pierwsze: wczytujemy Facebook's Bart Large MNLI z repozytorium Hugging Face i i odpowiednio inicjalizujemy go do celów klasyfikacji, dzięki Transformer Pipeline:

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

A później używamy modelu poprzez robienie tego:

classifier(user_request_in.text, user_request_in.labels)

Druga ważna rzecz: przeprowadzamy walidację danych dzięki Pydantic. Pydantic zmusza Cię do zadeklarowania z góry formatu wejściowego i wyjściowego dla Twojego API, co jest świetne z punktu widzenia dokumentacji ale także dlatego, że ogranicza potencjalne błędy. W Go zrobiłbyś prawie to samo z JSON unmarshalling za pomocą structs. Oto prosty sposób, aby zadeklarować, że pole "text" powinno mieć co najmniej 1 znak: constr(min_length=1). A poniższa instrukcja określa, że wejściowa lista etykiet powinna zawierać jeden element: conlist(str, min_items=1). Ta linia oznacza, że pole wyjściowe "labels" powinno być listą łańcuchów: List[str]. A ten oznacza, że wyniki powinny być listą float: List[float]. Jeżeli model zwróci wyniki, które nie są zgodne z tym formatem, FastAPI automatycznie podniesie błąd.

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

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

Na koniec, poniższy dekorator pozwala łatwo określić, że akceptujesz tylko żądania POST, na konkretnym punkcie końcowym: @app.post("/entities", response_model=EntitiesOut).

Bardziej zaawansowana walidacja danych

Możesz zrobić wiele bardziej złożonych rzeczy związanych z walidacją, takich jak na przykład kompozycja. Na przykład, powiedzmy, że robisz Named Entity Recognition (NER), więc twój model zwraca listę encji. Każda encja miałaby 4 pola: text, type, start oraz position. Oto jak możesz to zrobić:

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

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

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

Do tej pory pozwalaliśmy Pydanticowi zajmować się walidacją. Działa to w większości przypadków, ale czasami możesz chcieć dynamicznie podnieść błąd samodzielnie w oparciu o złożone warunki, które nie są natywnie obsługiwane przez Pydantic. Na przykład, jeśli chcesz ręcznie zwrócić błąd HTTP 400, możesz wykonać następujące czynności:

from fastapi import HTTPException

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

Oczywiście możesz zrobić o wiele więcej!

Ustawianie ścieżki głównej

Jeśli używasz FastAPI za odwrotnym proxy, najprawdopodobniej będziesz musiał pobawić się ścieżką root.

Trudność polega na tym, że za odwrotnym proxy aplikacja nie zna całej ścieżki URL, więc musimy jej jawnie powiedzieć, która to jest.

Na przykład tutaj pełny URL do naszego punktu końcowego może nie być po prostu /classification. Ale może to być coś w rodzaju /api/v1/classification. Nie chcemy na sztywno kodować tego pełnego adresu URL w celu aby nasz kod API był luźno powiązany z resztą aplikacji. Moglibyśmy to zrobić:

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

Lub alternatywnie możesz przekazać parametr do Uvicorn podczas uruchamiania:

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

Wniosek

Mam nadzieję, że udało nam się pokazać jak wygodne może być FastAPI dla API do przetwarzania języka naturalnego. Pydantic sprawia, że kod bardzo ekspresyjny i mniej podatny na błędy.

FastAPI ma świetną wydajność i umożliwia korzystanie z Python asyncio po wyjęciu z pudełka, co jest świetne dla wymagających modeli uczenia maszynowego, takich jak modele przetwarzania języka naturalnego oparte na Transformerach. Używamy FastAPI od prawie 1 rok w NLP Cloud i do tej pory nigdy nie byliśmy rozczarowani.

Jeśli jakieś pytanie, proszę nie wahaj się zapytać, to będzie przyjemność komentować!

Julien Salinas
CTO w NLP Cloud