Środowisko produkcyjne z modelem ML

Cel

Celem laboratorium jest zapoznanie studentów z tworzeniem aplikacji REST API w Pythonie z wykorzystaniem biblioteki Flask oraz jej konteneryzacją w Dockerze. Nauczysz się: - Tworzenia prostego REST API, - Obsługi zapytań HTTP i obsługi błędów w API, - Testowania API z wykorzystaniem pytest, - Przenoszenia aplikacji do kontenera Docker.

1. Tworzenie aplikacji REST API

Naszym zadaniem jest wystawienie aplikacji w Pythonie, która na żądanie klienta udzieli odpowiedzi na podstawie predykcji wygenerowanej przez model.

Aplikację napiszemy w Pythonie z wykorzystaniem Flask 3.0.3.

Kod minimalnej aplikacji Flask

Naszą aplikację chcemy uruchomić lokalnie, a następnie w prosty sposób przenieść i uruchomić na dowolnym komputerze. Dlatego naturalnym rozwiązaniem jest zapisanie kodu w pliku z rozszerzeniem .py.

Aby automatycznie zapisać kod aplikacji do pliku app.py, wykorzystamy magiczną komendę %%file plik.py.

%%file app.py
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def home():
    return jsonify({"message": "Hello, World!"})

if __name__ == '__main__':
    app.run()
Writing app.py

Uwaga! W dokumentacji Flask w kodzie podstawowej aplikacji nie występują dwie ostatnie linie odpowiedzialne za uruchomienie serwera.

if __name__ == '__main__':
    app.run()

Wyjaśnijmy co zawiera przykładowy kod.

  1. from flask import Flask Załadowanie biblioteki
  2. app = Flask(__name__) utworzenie interfejsu serwera API
  3. kod podstrony z wykorzystaniem dekoratora
@app.route('/')
def home():
    return jsonify({"message": "Hello, World!"})

Dekoratory w Pythonie pozwalają modyfikować zachowanie funkcji bez zmiany jej kodu. Flask wykorzystuje dekoratory do tworzenia tras (@app.route), ale można je także stosować w analizie danych – np. do logowania czasu wykonania funkcji lub obsługi błędów.

Przykład: Normalizacja wartości w danych

Załóżmy, że mamy funkcję, która pobiera dane z pliku CSV i zwraca listę wartości. Dodamy dekorator, który automatycznie przeskaluje dane do zakresu 0-1, co często jest wymagane przed analizą statystyczną lub trenowaniem modeli ML.

import numpy as np

# Dekorator do normalizacji danych
def normalize_data(func):
    def wrapper(*args, **kwargs):
        data = func(*args, **kwargs)  # Pobranie oryginalnych danych
        min_val, max_val = min(data), max(data)
        normalized = [(x - min_val) / (max_val - min_val) for x in data]
        print("Dane po normalizacji:", normalized)
        return normalized
    return wrapper


@normalize_data
def get_data():
    return [10, 15, 20, 30, 50]

get_data()
Dane po normalizacji: [0.0, 0.125, 0.25, 0.5, 1.0]
[0.0, 0.125, 0.25, 0.5, 1.0]

Ćwiczenie: „Napisz dekorator, który zaokrągla wartości do 2 miejsc po przecinku.”

Obsługa błędów w API

Dodajmy obsługę błędów, np. kiedy klient poda niepoprawne dane:

@app.errorhandler(404)
def not_found(error):
    return jsonify({"error": "Not Found"}), 404

@app.errorhandler(400)
def bad_request(error):
    return jsonify({"error": "Bad Request"}), 400

Uruchomienie serwera lokalnie

Uruchomienie serwera moze odbyć się na przynajmniej na dwa sposoby.

Uruchomienie serwera przez terminal

Otwórz termianal w lokalizacji gdzie znajduje się plik aplikacji

python app.py

lub (jeśli nie ma fragmentu app.run())

flask run

Powinna pojawić się informacja podobna do ponizszej:

 * Serving Flask app 'app'
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
Press CTRL+C to quit

W celu weryfikacji działania możesz otworzyć nowe okno terminalu wpisując:

curl localhost:5000
\{"message":"Hello, World!"\}

Uruchomienie serwera w notatniku

Bezpośrenie uruchomienia kodu w notatniku spowoduje uruchomienie serwera i zatrzymanie jakiejkolwiek mozliwości realizacji kodu. Aby tego uniknąć mozesz wykorzystać bibliotekę subprocess.

import subprocess
p = subprocess.Popen(["python", "app.py"])

Jeśli potrzebujemy zamknąć subprocess wykonaj:

p.kill()

2. Testowanie API

Do testowania API wykorzystamy pytest oraz bibliotekę requests. ### Instalacja pytest:

!pip install pytest requests -q
%%file test_app.py
import pytest
import requests

def test_home():
    response = requests.get("http://127.0.0.1:5000/")
    assert response.status_code == 200
    assert response.json()["message"] == "Hello, World!"
Writing test_app.py
!pytest test_app.py
============================= test session starts ==============================
platform linux -- Python 3.11.6, pytest-8.3.5, pluggy-1.5.0
rootdir: /home/jovyan/notebooks
plugins: anyio-4.0.0
collected 1 item                                                               

test_app.py .                                                            [100%]

============================== 1 passed in 0.03s ===============================
# wersja bez testu
import requests
response = requests.get("http://127.0.0.1:5000/")
print(response.json())
{'message': 'Hello, World!'}

Środowisko Python

Aby uruchomić kod aplikacji app.py, potrzebujemy interpretera języka Python zainstalowanego na naszym komputerze. Jednak samo posiadanie interpretera nie jest wystarczające – aby aplikacja działała poprawnie, należy utworzyć środowisko (najlepiej wirtualne), w którym będą dostępne wszystkie wymagane biblioteki, takie jak Flask.

uwaga: wszystkie polecenia terminala dotyczyć będą wersji linux/mac os

W pierwszej kolejności sprawdź czy dostępne są polecenia pozwalające realizować kod pythonowy.

which python
which python3
which pip 
which pip3

Wszystkie te polecenia powinny wskazyać na folder z domyślnym środowiskiem Pythona.

Wygeneruj i uruchom środowisko wirtualne lokalnie wpisując w terminalu:

python3 -m venv .venv
source .venv/bin/activate

Dobra praktyka: środowisko python to nic innego jak katalog. W naszej wersji to katalog ukryty o nazwie .venv. Jeśli skopiujesz ten katalog gdzie indziej przestanie pełnić on swoją funkcję środowiska python. Dlatego jego odtworzenie nie polega na jego kopiowaniu. Jeśli Twój projekt jest powiązany ze środowiskiem kontroli wersji GIT zadbaj aby katalog środowiska nie był dodawany do repozytorium. Mozesz wykonać to działanie dodając odpowiedni wpis do pliki .gitignore

Posiadając utworzone nowe środowisko sprawdź jakie biblioteki się w nim znajdują.

pip list 

Package    Version
---------- -------
pip        23.2.1
setuptools 65.5.0

Mozemy ponownie sprawdzić polecenia python i pip:

which python
which pip 

Domyślnie powinny pojawić się biblioteki pip oraz setuptools.

Doinstaluj bibliotekę flask.

pip install flask==3.0.3
pip list 
Package      Version
------------ -------
blinker      1.7.0
click        8.1.7
Flask        3.0.3
itsdangerous 2.1.2
Jinja2       3.1.3
MarkupSafe   2.1.5
pip          23.2.1
setuptools   65.5.0
Werkzeug     3.0.2

Jak widać instalacja biblioteki flask wymusiła doinstalowanie równiez innych pakietów.

Jedyną mozliwością przeniesienia środowiska python jest jego ponowna instalacja na nowej maszynie i instalacja wszystkich pakietów. Aby jednak nie instalować kazdego pakietu osobno mozemy wykorzystać plik konfiguracyjny requirements.txt zawierający listę pakietów.

Pamiętaj - kazdy pakiet powinien zawierać nr wersji pakietu. W innym przypadku moze okazać się, ze nowe werjse pakietów spowodują brak obsługi twojego kodu.

Aby utworzyć plik konfiguracyjny uzyj polecenia w terminalu:

pip freeze >> requirements.txt

Tak wygenerowany plik mozesz uzywać na dowolnej maszynie do instalacji i odtworzenia potrzebnego środowiska wykonawczego python.

Dygresja. W momencie przygotowywania materiałów Flask był w wersji 3.0.1 - dziś juz realizowany jest w wersji 3.0.3. Zmiany następują szybciej niz się wydaje. Instalacja pakietów z pliku odbywa się z wykorzystaniem polecenia:

pip install -r requierements.txt

Mamy teraz dwa pliki: app.py, i requirements.txt. Przenosząc je do dowolnego projektu na serwerach github jesteśmy w stanie uruchomić naszą aplikację wszędzie tam gdzie dostępny będzie interpreter python na którym mozemy utworzyć nowe wirtualne środowisko i zainstalować biblioteki z pliku requirements.txt.

Do pełnej automatyzacji przydałaby się jeszcze mozliwość uruchomienia środowiska python na dowolnej maszynie.

W tym celu utwórz plik Dockerfile:

%%file Dockerfile
FROM python:3.11-slim-buster

WORKDIR /app

COPY requirements.txt requirements.txt

RUN pip install -r requirements.txt

COPY app.py .

ENV FLASK_APP=app

EXPOSE 5000
CMD ["flask", "run", "--host", "0.0.0.0", "--port", "5000"]

Powyzszy plik pozwala w docker desktop uruchomić obraz wykorzystujący podstawowy system operacyjny (tutaj linux) wraz z podstawowym środowiskiem python3.11.

Ponadto plik ten kopiuje potrzebne pliki (app.py, requirements.txt) na obraz dockera.

Polecenie RUN pozwala uruchomić dowolne polecenie bash wewnątrz obrazu dockera.

Polecenie CMD pozwala uruchomić polecenie uruchamiające serwer w trybie tak by nie zamknąć tego polecenia.

Ostatnią informacją jest ustalenie portu na 8000.

utworzenie kontenera na podstawie pliku Dockerfile

docker build -t modelML .

uruchomienie kontenera

docker run -p 8000:8000 modelML