Jak serwować modele ML dzięki FastAPI?

miroslawmamczur.pl 9 miesięcy temu

– 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, iż 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, iż 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żeli 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ś prawdopodobnie była to skomplikowana sprawa i wymagała mnóstwa umiejętności. Ale zaraz pokażę Ci, iż 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żeli 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, iż 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, iż 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, iż 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ć, iż 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.

źródło

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

Gołym okiem widzimy, iż 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, iż dla powyższych wartości model przewiduje, iż 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, iż 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 prawdopodobnie 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, iż 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,

Idź do oryginalnego materiału