Przejdź do treści

Jak serwować modele ML za pomocą FastAPI?

– Tato, Ty jeszcze przy komputerze? Obiadek już czeka! – przybiegła Jagódka.

– Zaraz przyjdę. Dokończę tylko jedno ważne zadanko. Muszę przygotować API dla mojego modelu – powiedziałem, po czym zobaczyłem zdziwioną twarz Jagódki. Wiedziałem, że nie zrozumiała nic z tego, co przed chwilą powiedziałem. Zacząłem więc mówić językiem zrozumiałym dla 8 latki:

Zbudowałem bardzo fajny model. Pomyśl, że jest to zabawka, która zna odpowiedź na każde pytanie. Nie chcę jej mieć tylko dla siebie, więc muszę udostępnić ją w jakimś domku. Tym domkiem będzie serwer. Każdy będzie mógł zadać lalce pytanie poprzez wysłanie do niej listu na adres domku. Potrzebujemy więc jeszcze listonosza, który dostarczy list a następnie zaniesie odpowiedź do dziecka, które zadało pytanie. Tym listonoszem jest właśnie API, które piszę.

Ojej, czyli tworzysz listonosza?

Nie do końca. Nie umiem robić takich rzeczy, więc ułatwiam sobie pracę poprzez korzystanie z gotowych rozwiązań – uśmiechnąłem się pod nosem myśląc o analogii FastAPI do InPosta.

– Tata, Jaga! OBIAD! – z oddali słychać donośny krzyk Otylki.

– Chodźmy Jagódko. Dokończę to później – odpowiedziałem nie mogąc się doczekać naszego codziennego rodzinnego obiadku.

Uwaga. Jeśli nie wiesz, czym jest API, to zapraszam do poprzedniego artykułu [LINK].

Wejdźmy do świata tworzenia API

W świecie uczenia maszynowego, tworzenie potężnych i precyzyjnych modeli to jedna strona medalu, a umiejętność dostarczenia ich do rzeczywistych aplikacji i usług to druga. Możemy porównać modele ML do skomplikowanych i wydajnych narzędzi, które potrafią wykonywać zadania na poziomie niemożliwym do osiągnięcia dla człowieka. Jednak, aby te narzędzia były naprawdę użyteczne, muszą być dostępne w sposób intuicyjny i wygodny dla innych usług i aplikacji.

W dzisiejszych czasach istnieje wiele różnych metod przeliczania modeli, od przetwarzania wsadowego (ang. batch processing) po wykorzystanie gotowych narzędzi, które same serwują model. Jednakże czasami napotykamy sytuacje, w których potrzebujemy bardziej spersonalizowanego podejścia. Wtedy stajemy przed wyzwaniem stworzenia własnego interfejsu API (Application Programming Interface), który pozwoli innym usługom lub aplikacjom pytać nasz model o wyniki predykcji.

Nie bój się! Kiedyś zapewne była to skomplikowana sprawa i wymagała mnóstwa umiejętności. Ale zaraz pokażę Ci, że dzięki aktualnym bibliotekom możemy obudować model w API przy użyciu naprawdę niewielkiej ilości kodu. No to do dzieła!

Jaki framework wybrać?

Zazwyczaj wprowadzenie w życie jest fazą zamykającą projekt Data Science, umożliwiającą złączenie modelu uczenia maszynowego z aplikacją online. To istotne zadanie, które wymaga specjalnej uwagi. Istnieje wiele popularnych narzędzi, które można użyć do osiągnięcia tego celu, takie jak Flask czy Django, będące frameworkami do tego właśnie stworzonymi.

Zazwyczaj, kiedy mówimy o dużych projektach, Django jest pierwszym wyborem, ale jego konfiguracja wymaga pewnego nakładu czasu. Natomiast, jeśli chodzi o szybkie wdrożenie modelu w aplikacji online, to zwykle stawiamy na Flask.

Istnieje jeszcze jeden framework, który zdobywa coraz większą popularność. Korzysta z niego wiele znanych firm, m.in. Netflix i Uber. Ten niezwykle popularny framework to FastAPI.

Czym jest FastAPI?

FastAPI to nowoczesny framework webowy napisany w języku Python, który umożliwia tworzenie interfejsów API w sposób efektywny i wydajny. Aktualnie, jedną z najczęściej wykorzystywanych bibliotek do obudowywania modeli ML w interfejsie API w języku Python jest właśnie FastAPI.

FastAPI zdobył dużą popularność dzięki swojej wydajności, prostocie użycia oraz wsparciu dla asynchroniczności, co sprawia, że jest atrakcyjny dla twórców aplikacji internetowych opartych na modelach ML.

FastAPI umożliwia tworzenie interfejsów API w sposób deklaratywny, co sprawia, że jest on intuicyjny i przyjazny dla programistów. Dodatkowo automatycznie generuje dokumentację interfejsu API, co ułatwia zrozumienie i korzystanie z udostępnianych funkcji modelu i pozwala od razu całość przetestować.

Instalacja FastAPI

Proces instalacji FastAPI przebiega tak samo, jak przy dowolnej innej bibliotece dostępnej dla języka Python.

pip install fastapi

Wraz z FastAPI musimy także zainstalować uvicorn, aby działał jako serwer.

pip install uvicorn

Hello world (GET) dla FastAPI

Napiszmy nasz pierwszy kod:

from fastapi import FastAPI

# Creating FastAPI instance
app = FastAPI()


# Creating a decorator that specifies that the function below supports 
# HTTP GET requests on the path "/".
@app.get("/")
async def root():
    return {"message": "Hello World"}

Ten kod tworzy prostą aplikację FastAPI, która odpowiada „Hello World” na żądania HTTP typu GET na głównej ścieżce „/”.

To teraz użyjmy narzędzia uvicorn do uruchomienia naszej pierwszej aplikacji FastAPI (kod wpisujemy w terminalu).

uvicorn step1:app --reload --port 8080

Teraz możemy otworzyć przeglądarkę pod adresem http://127.0.0.1:8000.

Zobaczymy tam odpowiedź w formacie JSON zwracającą naszą wiadomość.

Interaktywna dokumentacja API

I teraz magia, którą bardzo lubię. Przejdź proszę na stronę http://127.0.0.1:8080/docs. Pojawia się tam automatycznie generowana dokumentacja przez Swager-UI.

Oczywiście każdą metodę GET czy POST, którą stworzymy można rozwinąć i przetestować. Na razie mamy tylko metodę GET to zobaczmy, jak to wygląda.

Możemy wykonać test poprzez naciśnięcie przycisku „Execute”. Dostajemy informację o odpowiedzi. W tym przypadku uzyskaliśmy „200”, czyli sukces! Mamy też „response body”, gdzie zwrócona została nam komunikacja i wyświetlony tekst „Hello World”.

Hello world (POST) dla FastAPI

Przeszliśmy przez żądanie GET. Teraz stwórzmy prosty przykład „Hello World” w FastAPI z użyciem żądania POST. Stworzymy endpoint, który przyjmuje dane tekstowe (imię) i zwraca spersonalizowane powitanie.

from fastapi import FastAPI

app = FastAPI()


# Creating an Endpoint to receive the data to make personalized greetings.
@app.post("/custom-greeting")
async def root(name):
    return {"message": f"Hello {name}"}

Najważniejszym elementem naszej funkcji jest personalizacja powitania. Wewnątrz niej podstawimy imię z przesłanych danych wejściowych, co pozwala nam stworzyć niepowtarzalne powitanie dostosowane do użytkownika.

Odpalamy ponownie dokumentację:

Rozwijamy naszą metodę:

Klikamy “Try it out” podając dane wejściowe i możemy przetestować metodę po naciśnięciu przycisku “Execute”.

Ostatecznie, nasz punkt końcowy zwraca rezultat w postaci słownika, gdzie spersonalizowane powitanie jest dostępne pod kluczem „message”. To nie tylko sprawia, że nasza usługa jest praktyczna, ale również czytelna dla klienta, który oczekuje personalizowanego feedbacku w odpowiedzi na swoje żądanie.

Struktura zapytania

Warto jeszcze dodać, że dobrą praktyką jest zdefiniowanie klasy dla zapytania, które przynosi kilka korzyści.

Po pierwsze, pomaga w jednoznacznej specyfikacji oczekiwanych danych wejściowych dla punktów końcowych (endpoints). Ułatwia to zrozumienie, jakie dane są wymagane, co z kolei pomaga uniknąć błędów i zapewnia spójność w interakcjach z API.

Po drugie, definiowanie struktur zapytania ułatwia walidację danych. FastAPI może automatycznie sprawdzać, czy przesłane dane zgadzają się z określoną strukturą, co pomaga w wykrywaniu ewentualnych błędów już na etapie obsługi zapytania. To zwiększa nie tylko niezawodność, ale także bezpieczeństwo aplikacji.

Stwórzmy zatem jeszcze strukturę zapytania:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


# Creating class to define the request body and the type hints of each attribute
class GreetingRequest(BaseModel):
    name: str


@app.post("/custom-greeting")
async def root(name: GreetingRequest):
    return {"message": f"Hello {name.name}"}

i spawdźmy szczegóły w dokumentacji:

Budowa naszego modelu do serwowania

Zbudujmy jakiś model, który chcielibyśmy na razie lokalnie serwować poprzez REST API. W tym artykule chciałem się skoncentrować na pokazaniu wam działania FastAPI, dlatego zbudujemy sobie model na podstawie kochanego przez wszystkich zbioru danych Iris (LINK), który jest tak rozpoznawalny jak Hołownia, gdy został marszałkiem sejmu.

W tym zbiorze kolekcji mamy trzy gatunki kwiatów Iris: Setosa, Versicolor i Virginica, z których każdy jest inny i wyjątkowy.

Naszym celem jest nauczenie modelu rozpoznawania tych kwiatów na podstawie ich atrybutów, takich jak szerokość i długość dwóch rodzajów płatków.

Zobaczmy, jak wygląda zbiór danych na podstawie długości i szerokości jednego płatka.

Gołym okiem widzimy, że można oddzielić klasę setosa od pozostałych.

Napiszmy funkcję zwracająca model i metrykę accurancy, gdzie na wejściu podajemy wielkość zbioru testowego branego domyślnie jako 50%.

# Import necessary libraries
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score

import pickle


def train_model(test_size=0.50):
    # Load the Iris dataset
    iris = load_iris()
    X = iris.data
    y = iris.target

    # Split the dataset into training and testing sets
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=2023)

    # Initialize the Random Forest classifier
    rf_classifier = RandomForestClassifier(n_estimators=100, random_state=42)

    # Train the model
    rf_classifier.fit(X_train, y_train)

    # Predict on the test set
    y_pred = rf_classifier.predict(X_test)
    accuracy = accuracy_score(y_test, y_pred)

    # Save the trained model to a file using pickle
    model_filename = "./model/random_forest_model.pkl"
    with open(model_filename, 'wb') as model_file:
        pickle.dump(rf_classifier, model_file)

    return rf_classifier, accuracy


model, accuracy = train_model()
print(f'{round(100*accuracy,2)}%')

I mamy przygotowany kodzik do trenowania modelu. W naszym przypadku model ma moc 97%.

Predict w FastAPI

Aby przygotować predykcję modelu musimy zrobić jeszcze kilka rzeczy.

Definiowanie Struktury Zapytania

We wcześniejszym kroku zbudowaliśmy najlepszy model na świecie. Mając model oczywiście wiemy, jakich zmiennych oczekuje. W naszym przypadku, aby wygenerować predykcje, potrzebujemy podać cztery wartości (długości i szerokości płatków).

Uwaga! Zawsze dbaj o to, aby kolejność była identyczna jak podczas budowy modelu! Dodatkowo warto zadbać, aby nie było zmiennych jakich model nie widział przy budowie, czyli np. zadbać o taką samą obsługę pustych wartości.

Aby zdefiniować strukturę zapytania tworzymy klasę RequestBody określając typy danych dla każdej zmiennej, jakiej wymagamy w modelu.

# Creating class to define the request body and the type hints of each attribute
class request_body(BaseModel):
    sepal_length: float
    sepal_width: float
    petal_length: float
    petal_width: float

Endpoint do predykcji

W przypadku predykcji będziemy chcieli wykorzystać żądania POST, ponieważ będziemy oczekiwać wartości, na podstawie której będziemy zwracać wynik predykcji.

Musimy stworzyć tak zwany endpoint /predict, który obsługuje żądania POST, zawierające dane do przewidzenia.

Funkcja predict w naszym przypadku:

  • przetwarza dane wejściowe i tworzy dokładnie taki wektor, który będziemy mogli wczytać przez model,
  • wczytuje zapisany wcześniej model,
  • dokonuje predykcji klasy,
  • zwraca wynik (numer klasy oraz nazwę).

Kod w Python

Połączmy wszystko w całość:

from fastapi import FastAPI
from sklearn.datasets import load_iris
from pydantic import BaseModel

import pickle

# Creating FastAPI instance
app = FastAPI()
iris = load_iris()


# Creating class to define the request body and the type hints of each attribute
class request_body(BaseModel):
    sepal_length: float
    sepal_width: float
    petal_length: float
    petal_width: float


# Creating an Endpoint to receive the data to make prediction on.
@app.post('/predict')
def predict(data: request_body):
    # Making the data in a form suitable for prediction
    test_data = [[
        data.sepal_length,
        data.sepal_width,
        data.petal_length,
        data.petal_width
    ]]

    # Load the saved model from file
    model_filename = "./model/random_forest_model.pkl"
    with open(model_filename, 'rb') as model_file:
        loaded_model = pickle.load(model_file)

    # Predicting the Class
    class_idx = loaded_model.predict(test_data)[0]

    # Return the Result
    return {'class_index': str(class_idx),
            'class_name': iris.target_names[class_idx]}

Odpalamy nasz projekt poprzez uvicorn (zapisałem pliczek jako step4-predict.py):

uvicorn step4-predict:app --reload --port 8080

i przetestujmy wynik w załączonej dokumentacji.

W pierwszym kroku przetestujmy dla samych wartości “1”:

i otrzymujemy wynik:

Widzimy, że dla powyższych wartości model przewiduje, że to powinien być irys septosa.

A co, gdyby zmienić na przykład ostatnie dwie wartości na 5?

Wówczas predykcja nam się zmienia na ‘virginica’.

HURA!!!! Mamy nasz model postawiony lokalnie! Nie było tak strasznie, prawda?

Trenowanie modelu

A pamiętacie jak napisaliśmy funkcje do trenowania modelu? Dlaczego nie dać możliwości użytkownikom samemu do przebudowania modelu, wymagajac od nich podania wielkości zbioru testowego jakiego oczekują.

Zadeklarujmy kolejny endpoint i wykorzystajmy naszą funkcję ‘train_model’, którą stworzyliśmy wcześniej. Dodajmy już do istniejącego kodu, co sprawi, że będziemy mieć dwa endpointy.

I przetestowaliśmy przebudowę modelu dla parametru test_size=90%.

Wszystko się zgadza z naszą intuicją. Moc modelu spadła, ponieważ zbiór do trenowania danych stanowił jedynie 10%.

Inne sposoby wywoływania naszego API

Jak zapewne zwróciliście uwagę na powyższych screenach z dokumentacji przy zapytaniu jest podany kod curlowy.

CURL

CURL to narzędzie wiersza poleceń, które pomaga wysyłać zapytania i odbierać odpowiedzi z serwerów w internecie. To jak magiczna różdżka dla komputerów, pozwalająca im komunikować się ze stronami internetowymi czy usługami online.

Gdy używasz CURLa, w zasadzie mówisz komputerowi, aby poszedł do określonej strony internetowej lub usługi i przyniósł z powrotem informacje. Możesz również używać go do wysyłania danych na serwer, na przykład gdy wypełniasz formularz online.

Wklejmy w wiersz poleceń jeszcze raz powyższe zapytanie o przebudowę modelu z innym parametrem i otrzymujemy wynik:

Jak widzicie, póki mamy odpalony lokalny serwer, to dzięki uvicorn możemy się z nim połączyć.

Request (python)

Oczywiście możemy też napisać prosty kod w python korzystający z biblioteki request do tego samego, aby wywołać naszego endpointa.

import requests

# defining URL
url = 'http://127.0.0.1:8080/rebuild'

# my sending data
data = {
    "test_size": 0.9
}

# declaring headers
headers = {
    'accept': 'application/json',
    'Content-Type': 'application/json'
}

# sending request
response = requests.post(url, json=data, headers=headers)

# printing answer as json
print(response.json())

Podsumowanie

Zanurzając się w świat FastAPI, odkryliśmy smakowite recepty dla tworzenia szybkich i efektywnych interfejsów API. Podobnie jak kucharz używa precyzyjnych składników, FastAPI pozwala nam elegancko komponować punkty końcowe z dynamicznymi danymi. W tej kulinarnej podróży nauczyliśmy się gotować z przyjemnością, tworząc interaktywne potrawy dla naszych aplikacji, a teraz tylko niebo jest granicą dla naszej kreatywności w kuchni kodu!

Pamiętajcie, że to tylko pierwszy krok. Warto zgłębiać dalej zagadnienia związane z API, w szczególności dotyczące zabezpieczania API (np. autoryzacja, uwierzytelnianie), limitowania dostępu, poznania mechanizmów optymalizujących wydajność, skalowanie na serwerach czy wystawianie w chmurze.

Pozdrawiam z całego serducha,

2 komentarze do “Jak serwować modele ML za pomocą FastAPI?”

  1. Pingback: Czym jest Docker? I jak uruchomić model uczenia maszynowego w kontenerze? - Mirosław Mamczur

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Spodobało Ci się? Udostępnij ten post w social mediach! ❤️❤️❤️