Η πρώτη έκδοση του FastAPI κυκλοφόρησε στα τέλη του 2018 και έκτοτε χρησιμοποιείται όλο και περισσότερο σε πολλές εφαρμογές παραγωγής (δείτε την ιστοσελίδα της FastAPI). Αυτό είναι αυτό που χρησιμοποιούμε πίσω από το καπό στο NLP Cloud. Είναι ένας πολύ καλός τρόπος για να εξυπηρετούμε εύκολα και αποτελεσματικά τους εκατοντάδες μοντέλα επεξεργασίας φυσικής γλώσσας, για εξαγωγή οντοτήτων (NER), ταξινόμηση κειμένου, ανάλυση συναισθήματος, ερωτήσεις απάντηση ερωτήσεων, περίληψη... Διαπιστώσαμε ότι το FastAPI είναι ένας πολύ καλός τρόπος για να εξυπηρετούμε βαθιά προγράμματα που βασίζονται σε μετασχηματιστές learning models.
Σε αυτό το άρθρο, σκεφτήκαμε ότι θα ήταν ενδιαφέρον να σας δείξουμε πώς υλοποιούμε ένα API Επεξεργασίας Φυσικής Γλώσσας που βασίζεται σε σε μετασχηματιστές Hugging Face με το FastAPI.
Πριν από το FastAPI, χρησιμοποιούσαμε ουσιαστικά το Django Rest Framework για τα Python APIs μας, αλλά γρήγορα γινόμασταν ενδιαφέρον για το FastAPI για τους ακόλουθους λόγους:
Αυτές οι εξαιρετικές επιδόσεις καθιστούν το FastAPI απόλυτα κατάλληλο για API μηχανικής μάθησης που εξυπηρετούν μοντέλα βασισμένα σε μετασχηματιστές όπως το δικό μας.
Για να λειτουργήσει το FastAPI, το συνδέουμε με τον διακομιστή ASGI του Uvicorn, ο οποίος είναι ο σύγχρονος τρόπος για να να χειρίζεστε εγγενώς ασύγχρονες αιτήσεις Python με το asyncio. Μπορείτε είτε να αποφασίσετε να εγκαταστήσετε το FastAPI με το Uvicorn χειροκίνητα ή να κατεβάσετε μια έτοιμη προς χρήση εικόνα Docker. Ας δείξουμε το χειροκίνητη εγκατάσταση πρώτα:
pip install fastapi[all]
Τότε μπορείτε να το ξεκινήσετε με:
uvicorn main:app
Ο Sebastián Ramírez, ο δημιουργός του FastAPI, παρέχει αρκετές έτοιμες εικόνες Docker που το καθιστούν πολύ εύκολη τη χρήση του FastAPI στην παραγωγή. Το Uvicorn + Gunicorn + FastAPI image εκμεταλλεύεται το Gunicorn προκειμένου να χρησιμοποιηθούν παράλληλα πολλές διεργασίες (δείτε την εικόνα εδώ). Τελικά, χάρη στην Uvicorn μπορείτε να χειρίζεστε πολλές περιπτώσεις FastAPI μέσα στην ίδια διαδικασία Python, και χάρη στο Gunicorn μπορείτε να δημιουργήσετε πολλές διεργασίες Python.
Η εφαρμογή FastAPI θα ξεκινήσει αυτόματα κατά την εκκίνηση του δοχείου Docker με τα εξής:
docker run.
Είναι σημαντικό να διαβάσετε σωστά την τεκμηρίωση αυτών των εικόνων Docker, καθώς υπάρχουν κάποιες ρυθμίσεις που
μπορεί να θέλετε να τροποποιήσετε, όπως για παράδειγμα τον αριθμό των παράλληλων διεργασιών που δημιουργεί το Gunicorn. Από προεπιλογή,
η εικόνα γεννά τόσες διεργασίες όσοι και ο αριθμός των πυρήνων της CPU στο μηχάνημά σας. Αλλά σε περίπτωση απαιτητικών
μοντέλων μηχανικής μάθησης, όπως οι μετασχηματιστές επεξεργασίας φυσικής γλώσσας, αυτό μπορεί γρήγορα να οδηγήσει σε δεκάδες GB μνήμης που χρησιμοποιούνται. Ένα
στρατηγική θα ήταν να αξιοποιήσετε την επιλογή Gunicorn (--preload), για να φορτώσει
το μοντέλο σας μόνο μία φορά στη μνήμη και να το μοιράζεται μεταξύ όλων των διεργασιών FastAPI Python. Μια άλλη επιλογή θα ήταν
να περιορίσετε τον αριθμό των διεργασιών του Gunicorn. Και οι δύο έχουν πλεονεκτήματα και μειονεκτήματα, αλλά αυτό είναι πέρα από το
του παρόντος άρθρου.
Η ταξινόμηση κειμένου είναι η διαδικασία προσδιορισμού του τι αναφέρεται σε ένα κείμενο (Space? Επιχειρήσεις; Τρόφιμα;...). Περισσότερες λεπτομέρειες για το κείμενο ταξινόμηση εδώ.
Θέλουμε να δημιουργήσουμε ένα τελικό σημείο API που εκτελεί ταξινόμηση κειμένου χρησιμοποιώντας το Bart Large MNLI του Facebook μοντέλο, το οποίο είναι ένα προ-εκπαιδευμένο μοντέλο που βασίζεται σε μετασχηματιστές Hugging Face, απόλυτα κατάλληλο για κείμενο ταξινόμηση κειμένου.
Το τελικό σημείο του API μας θα δέχεται ένα κομμάτι κειμένου ως είσοδο, μαζί με πιθανές κατηγορίες (που ονομάζονται ετικέτες), και θα επιστρέφει μια βαθμολογία για κάθε κατηγορία (όσο υψηλότερη, τόσο πιο πιθανή).
Θα ζητήσουμε το τελικό σημείο με αιτήματα POST ως εξής:
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"]
}'
Και σε αντάλλαγμα θα παίρναμε μια απάντηση όπως:
{
"labels": [
"job",
"space",
"nature"
],
"scores": [
0.9258803129196167,
0.19384843111038208,
0.010988432914018631
]
}
Δείτε πώς μπορείτε να το πετύχετε με το FastAPI και τους Transformers:
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)
Πρώτα απ' όλα: φορτώνουμε το Bart Large MNLI του Facebook από το αποθετήριο Hugging Face, και το αρχικοποιούμε σωστά για σκοπούς ταξινόμησης, χάρη στο Transformer Pipeline:
classifier = pipeline("zero-shot-classification",
model="facebook/bart-large-mnli")
Και αργότερα χρησιμοποιούμε το μοντέλο κάνοντας αυτό:
classifier(user_request_in.text, user_request_in.labels)
Δεύτερο σημαντικό πράγμα: πραγματοποιούμε επικύρωση δεδομένων χάρη στο Pydantic. Το Pydantic σας αναγκάζει να
να δηλώσετε εκ των προτέρων τη μορφή εισόδου και εξόδου για το API σας, κάτι που είναι σπουδαίο από πλευράς τεκμηρίωσης
άποψη, αλλά και επειδή περιορίζει τα πιθανά λάθη. Στην Go θα κάνατε σχεδόν το ίδιο πράγμα
με το JSON unmarshalling με structs. Εδώ είναι ένας εύκολος τρόπος για να
να δηλώσετε ότι το πεδίο "text" πρέπει να έχει τουλάχιστον 1 χαρακτήρα: constr(min_length=1).
Και το ακόλουθο ορίζει ότι η λίστα εισόδου των ετικετών πρέπει να περιέχει ένα στοιχείο:
conlist(str,
min_items=1).
Αυτή η γραμμή σημαίνει ότι το πεδίο εξόδου "labels" θα πρέπει να είναι μια λίστα συμβολοσειρών: List[str].
Και αυτό σημαίνει ότι οι βαθμολογίες πρέπει να είναι μια λίστα από floats: List[float].
Εάν το μοντέλο επιστρέφει αποτελέσματα που δεν ακολουθούν αυτή τη μορφή, το FastAPI θα δημιουργήσει αυτόματα ένα σφάλμα.
class UserRequestIn(BaseModel):
text: constr(min_length=1)
labels: conlist(str, min_items=1)
class ScoredLabelsOut(BaseModel):
labels: List[str]
scores: List[float]
Τέλος, ο ακόλουθος διακοσμητής διευκολύνει τον καθορισμό ότι δέχεστε μόνο αιτήσεις POST, σε ένα συγκεκριμένο τελικό σημείο:
@app.post("/entities", response_model=EntitiesOut).
Μπορείτε να κάνετε πολλά πιο σύνθετα πράγματα επικύρωσης, όπως για παράδειγμα σύνθεση. Για παράδειγμα, ας πούμε ότι
κάνετε αναγνώριση ονομαστικών οντοτήτων (NER), οπότε το μοντέλο σας επιστρέφει μια λίστα οντοτήτων. Κάθε οντότητα
θα έχει 4 πεδία: text, type, start και position. Ακούστε πώς θα μπορούσατε να το κάνετε:
class EntityOut(BaseModel):
start: int
end: int
type: str
text: str
class EntitiesOut(BaseModel):
entities: List[EntityOut]
@app.post("/entities", response_model=EntitiesOut)
# [...]
Μέχρι τώρα, αφήναμε το Pydantic να χειριστεί την επικύρωση. Λειτουργεί στις περισσότερες περιπτώσεις, αλλά μερικές φορές μπορεί να θέλετε να εγείρετε δυναμικά ένα σφάλμα μόνοι σας με βάση σύνθετες συνθήκες που δεν αντιμετωπίζονται εγγενώς από το Pydantic. Για παράδειγμα, αν θέλετε να επιστρέψετε χειροκίνητα ένα σφάλμα HTTP 400, μπορείτε να κάνετε τα εξής:
from fastapi import HTTPException
raise HTTPException(status_code=400,
detail="Your request is malformed")
Φυσικά μπορείτε να κάνετε πολύ περισσότερα!
Αν χρησιμοποιείτε το FastAPI πίσω από έναν αντίστροφο διακομιστή μεσολάβησης, πιθανότατα θα χρειαστεί να παίξετε με τη διαδρομή ρίζας.
Το δύσκολο είναι ότι, πίσω από έναν αντίστροφο διακομιστή μεσολάβησης, η εφαρμογή δεν γνωρίζει ολόκληρη τη διαδρομή URL, οπότε πρέπει να της πούμε ρητά ποια είναι.
Για παράδειγμα, εδώ η πλήρης διεύθυνση URL για το τελικό μας σημείο μπορεί να μην είναι απλά /classification. Αλλά θα μπορούσε να είναι κάτι σαν /api/v1/classification. Δεν θέλουμε να κωδικοποιήσουμε αυτό το πλήρες URL προκειμένου να
ο κώδικάς μας API να είναι χαλαρά συνδεδεμένος με την υπόλοιπη εφαρμογή. Θα μπορούσαμε να κάνουμε αυτό:
app = FastAPI(root_path="/api/v1")
Εναλλακτικά, θα μπορούσατε να περάσετε μια παράμετρο στο Uvicorn κατά την εκκίνησή του:
uvicorn main:app --root-path /api/v1
Ελπίζω να σας δείξαμε με επιτυχία πόσο βολικό μπορεί να είναι το FastAPI για ένα API επεξεργασίας φυσικής γλώσσας. Το Pydantic κάνει τον κώδικα πολύ εκφραστικό και λιγότερο επιρρεπή σε σφάλματα.
Το FastAPI έχει εξαιρετικές επιδόσεις και καθιστά δυνατή τη χρήση της Python asyncio out of the box, η οποία είναι υπέροχη. για απαιτητικά μοντέλα μηχανικής μάθησης, όπως τα μοντέλα επεξεργασίας φυσικής γλώσσας που βασίζονται σε Transformer. Χρησιμοποιούμε το FastAPI για σχεδόν 1 χρόνο στο NLP Cloud και δεν έχουμε απογοητευτεί ποτέ μέχρι στιγμής.
Αν έχετε κάποια ερώτηση, μη διστάσετε να ρωτήσετε, θα χαρώ να σχολιάσω!
Julien Salinas
CTO στο NLP Cloud