Erstelle eine API mit Flask
Nachdem wir ein eigenes Modell erstellt haben, können wir uns nun damit beschäftigen, wie wir dieses veröffentlichen, um es in verschiedenen Anwendungen nutzen zu können. Als ersten Schritt benötigen wir dazu eine API, die später über das Internet erreichbar sein soll. Es gibt für Python verschiedene Frameworks, die uns bei der Erstellung unterstützen können. Zwei der bekanntesten sind Django und Flask. Wir nutzen in diesem Tutorial Flask, welches sehr schlank ist und dadurch einen schnellen Einstieg ermöglicht.
Wir werden uns jedoch ausschließlich mit der Programmierung des Backends beschäftigen. Über den Browser kann dieses am Ende des Kapitels zwar schon getestet werden, für eine Veröffentlichung wird allerdings ein zusätzliches Frontend, beispielsweise in Form einer Webseite oder App, benötigt, welches dann auf unser Backend zurückgreift.
Was ist Flask?
Welcome to Flask - Flask Documentation
Flask ist ein extrem schlankes Webframework und wird mit einem eigenen Webserver für die Entwicklung ausgeliefert. Dadurch ist es möglich sehr schnell ein erstes eigenes Webinterface bereitzustellen. Die wenigen Vorgaben seitens Flask lassen viel Spielraum für eigene individuelle Lösungen, aber sorgen dadurch auch nicht automatisch für einheitlichen sauberen Code. Flask nutzt die Template Engine Jinja mit der auch ein Frontend gebaut werden könnte. Der mitgelieferte Webserver ist nur für die Entwicklungszeit gedacht und sollte bei der Veröffentlichung durch eine stabilere und sicherere Alternative ersetzt werden. Wir werden zusätzlich das Package Flask CORS einführen, welches nötig ist, wenn Frontend und Backend nicht auf dem gleichen Server laufen.
Flask und Flask CORS können direkt über pip
installiert werden:
pip install Flask
pip install flask-cors
Grundgerüst aufbauen
Die gesamte Funktionalität unserer App ist in dem Ordner 3_1_Flask/app
gespeichert. Diese werden wir später im Docker benutzen. Die App beinhaltet neben main.py
auch die Ordner models
, in dem wir unser Modell aus 1_2_Tensorflow abspeichern, und utils
, in dem die bekannte Datei utils.py
liegt.
Wir bauen und erklären in diesem Tutorial die Datei main.py
. Die Datei kann hier in unserem Git eingesehen werden.
Zunächst importieren wir alle benötigten Packages:
Anschließend kann ein neues App-Objekt angelegt werden. Zusätzlich verwenden wir CORS, um unser Backend auch zusammen mit einem separaten Frontend nutzen zu können. Wird das Frontend beispielsweise direkt auch mit der Jinja Template Engine und Flask erstellt, ist dieser Schritt nicht notwendig und das flask_cors
Package wird nicht benötigt. Da in den meisten Fällen jedoch eine App oder ähnliches als Frontend dient und es in dem Fall sonst zu Problemen kommen kann, wird dieses Package hier vorgestellt. Alle späteren Anweisungen sind jedoch unabhängig von diesem Schritt.
Grundsätzlich funktioniert Flask sehr einfach. Es können normale Python-Funktionen geschrieben werden, die lediglich mit einem Decorator ergänzt werden müssen. In diesem wird der Pfad angegeben und optional können weitere Einstellungen getätigt werden. Der Rückgabewert der Funktion wird als Antwort auf die Anfrage zurückgesandt. Flask bietet Funktionen wie jsonify()
an, die Python Objekte entsprechend vorbereiten und dazu im konkreten Fall in ein JSON-Objekt umwandeln:
Da wir auch in diesem Fall eine Vorhersage mithilfe der fertig trainierten Modelle machen wollen, können wir uns an unserem Test-Skript (siehe hier) orientieren. Alle benötigten Modelle hatten wir bereits als Dateien gespeichert. Auf sie wird während der Predictions nur noch lesend zugegriffen, weshalb es ausreichend ist, wenn wir sie einmalig zu Beginn einlesen:
Anschließend benötigen wir noch eine Schnittstelle, über die Anfragen für eine Preisvorhersage an unseren Server geschickt werden können. Im Gegensatz zu unserer hello_world()
Funktion sollen diese Anfragen jedoch auch nutzerspezifische Parameter enthalten.
Eingaben entgegennehmen
Um Eingaben entgegenzunehmen, gibt es verschiedene Möglichkeiten. Eine einfache und häufig sinnvolle Variante ist die explizite Angabe der Parameter als Teil des Pfades. Wir erweitern dazu unser Hello World-Beispiel zu einer individuellen Grüß-Funktion:
Flask erkennt die entsprechenden Teile des Pfades und achtet dabei sogar auf den Datentyp. Die gefundenen Werte werden unserer Funktion dann als normale Parameter übergeben, sodass diese wie bei jeder anderen Python-Funktion einfach verwendet werden können. In unserem Fall erwarten wir jedoch acht verschiedene Eingaben, deren Reihenfolge bei dieser Methode exakt stimmen muss. Wir haben uns deshalb für eine andere Variante entschieden.
Wie in dem Decorator oben bereits zu sehen ist, haben wir dieses Mal einen zusätzlichen Parameter methods=['GET']
gesetzt. Auf diese Weise ist es möglich die HTTP-Methode auszuwählen. Standardmäßig wird bereits GET
verwendet, aber es wäre ebenso möglich POST
anzugeben oder mehrere Methoden zu erlauben, die dann erst in der Funktion entsprechend bearbeitet werden. Hätten wir als Frontend beispielsweise ein HTML-Formular, würde sich POST
eher anbieten. Da wir jedoch noch kein Frontend besitzen, aber unsere API später gerne einfach testen würden, verwenden wir GET
. Dadurch können wir unsere Parameter in einem Query-String direkt als Teil unserer URL kodieren und diese mit einem Browser ohne weitere Hilfsmittel aufrufen.
Ein Query-String ist Teil einer URL und kann mehrere benannte Parameter enthalten. Als Markierung des Beginns dient ein ?
. Anschließend folgt der Query-String selbst bestehend aus Paaren field1=value1
, die mithilfe von &
oder ;
verbunden werden. Da alle Namen und Werte Teil der URL sind, müssen diese entsprechend kodiert werden. So wird beispielsweise ein Leerzeichen durch +
oder %20
ersetzt.
Wir können bei dieser Methode keine genauen Angaben machen, welche Parameter wir brauchen. Stattdessen stellt uns Flask alle übergebenen Parameter in einem Request Object als eine Art Dictionary bereit.
Da wir als Eingabe für unser Modell immer alle Parameter benötigen und zudem auch die Datentypen stimmen müssen, überprüfen wir beides bevor wir die Parameter weiterverarbeiten:
Für das Automodell erwarten wir beispielsweise den Parameter model
. Wir prüfen zunächst, ob dieser überhaupt in der Anfrage enthalten ist. Wenn dies der Fall ist, stellen wir anschließend noch sicher, dass das angegebene Automodell von unserem Encoder auch konvertiert werden kann bzw. es in unseren Trainingsdaten enthalten war. Ist eine der Bedingungen nicht erfüllt, können wir mit Flask einfach eine Fehlermeldung mit einem entsprechenden Fehlercode zurückgeben. Zusätzlich sollten wir das Problem kurz und konkret benennen, um die spätere Benutzung zu vereinfachen.
Kommt es während des Programmablaufs zu unerwarteten Fehlern, sodass unsere Methode nicht korrekt beendet werden kann, kümmert sich Flask darum, eine Fehlermeldung an den Client zurückzusenden. Dabei handelt es sich meist um den Fehlercode 500 Internal Server Error. Da (außerhalb des Debug-Modus) ansonsten jedoch keine genauere Fehlerbeschreibung übermittelt wird, ist eine genauere Identifikation des Problems für den Client nicht möglich (aber häufig natürlich auch nicht gewünscht). Wir wollen in unserem Fall den Client auf eine fehlerhafte Eingabe hinweisen und ihn möglichst bei der Korrektur unterstützen. Dafür verwenden wir den Fehlercode 400 Bad Request und ergänzen eine konkrete fehlerspezifische Nachricht.
Ist der Parameter vorhanden und gültig, konvertieren und speichern wir diesen zwischen, um auch die weiteren Parameter analog zu prüfen. Haben wir dies für alle Parameter getan, können wir unser Dataframe konstruieren und mithilfe unseres Modells einen möglichen Preis vorhersagen:
Die Schritte hierfür sind nahezu identisch zu unserem Test-Skript (siehe hier), weshalb wir auf diese nicht weiter eingehen. Lediglich die letzten beiden Zeilen haben wir ergänzt, um die Rückgabe unseres Servers etwas schöner zu machen. So runden wir unsere Vorhersage auf zwei Nachkommastellen und setzen zudem negative Vorhersagen auf 0. Den resultierenden Preis verpacken wir zudem wieder in ein JSON-Dictionary. Natürlich können hier jedoch auch vollkommen andere Antworten konstruiert werden. Es ist lediglich wichtig, auf eine Absprache zwischen Front- und Backend zu achten.
Flask ausführen
Unsere neu geschriebene API können wir dank des in Flask mitgelieferten Webservers direkt auf unserem Computer testen. Hierfür reicht der Aufruf von run()
auf unserem zu Beginn erzeugten app-Objekt:
Wir können unser Skript dann einfach wie jedes andere Python-Skript ausführen. Während der Entwicklung ist es zudem sinnvoll den Parameter debug=True
zu setzen. Dadurch zeigt Flask bei Problemen die komplette Fehlermeldung direkt im Browser an und versteckt diese nicht hinter dem zuvor erwähnten generischen Server Error. Wird kein anderer Port angegeben, läuft der Flask Server üblicherweise auf Port 5000. Du erreichst diesen also unter localhost:5000
. Falls du keine Methode für den Root-Pfad / geschrieben hast, wirst du unter dieser Adresse allerdings eine Fehlermeldung erhalten. Du kannst jedoch einfach einen entsprechenden Pfad ergänzen. Für die URL http://localhost:5000/hello/REACH erhalten wir beispielsweise die folgende Antwort:
Der Server ist bisher jedoch nur auf deinem lokalen Computer verfügbar. Um dies zu ändern, kannst du einen Host angeben. Die Adresse 0.0.0.0 sorgt dafür, dass dein Server über alle öffentlichen IP-Adressen deines Systems erreichbar sein soll. Dies solltest du natürlich nur tun, wenn du den anderen Teilnehmenden in deinem Netzwerk vertraust. Abhängig von den sonstigen Netzwerkeinstellungen kannst du dann jedoch auch von anderen Geräten in deinem Netzwerk deinen Server aufrufen.
Wir können nun unser Modell testen, indem wir eine entsprechende Anfrage als Query-String senden (wir benutzen hier dieselben Eingaben wie im Tensorflow-Tutorial): http://localhost:5000/api/car-price?model=T-Roc&year=2019&transmission=manual&mileage=12123&fuelType=petrol&tax=145&mpg=42.7&engineSize=2.0. Als Ausgabe erhalten wir wie erwartet:
Bei ungültigen Eingaben (http://localhost:5000/api/car-price?model=Cybertruck) erhalten wir stattdessen den Fehlercode 400 und unsere zuvor definierte Fehlermeldung:
Produktionsserver
Statt über einen Methodenaufruf im Skript kannst du deinen Server auch über einen eigenen Flask-Befehl in der Kommandozeile starten. Hierzu musst du jedoch zunächst den Namen deines Skripts in einer Environment-Variable FLASK_APP
hinterlegen. Der Befehl lautet anschließend einfach flask run
.
Da der mitgelieferte Server jedoch nur für die Entwicklung gedacht ist und schlecht skaliert, sollten wir diesen durch einen anderen Server ersetzen. Es gibt zahlreiche Alternativen, wir werden Gunicorn verwenden, da dieser gut zusammen mit Flask funktioniert und ebenfalls einfach einzurichten ist. Der Server funktioniert nur auf UNIX Systemen und kann dort einfach mit pip
installiert werden. Nachdem wir in den Ordner /app
gewechselt sind, reicht anschließend ein einzelner Aufruf in der Kommandozeile, ähnlich zu dem Flask-eigenen Server:
Da wir unseren Server später innerhalb eines Docker Containers und ohnehin nicht auf unserem eigenen Computer betreiben wollen, ist es nicht schlimm, wenn die Entwicklung auf einem nicht-UNIX System erfolgt oder Gunicorn auf deiner lokalen Maschine nicht funktioniert.