من خوارزمية AI إلى API كاملة الوظيفة

Ismail Mebsout
٢٣ أكتوبر ٢٠٢٤
١٠ دقائق
جدول المحتويات

بصفتك Data Scientist، عند العمل على مشروع معقّد إلى جانب مطوّرين آخرين، تحتاج في كثير من الأحيان إلى تغليف خوارزمية AI الخاصّة بك فيما نُسمّيه API يمكن استدعاؤه من قِبَل backend لتنسيق تطبيقك. يحمل استخدام API عدّة مزايا تجعل تنبّؤاتك أكثر كفاءة وأقلّ استهلاكًا للوقت.

في هذا المقال، سنتطرّق إلى تعريف API بشكل عامّ مع التعمّق في RESTful، ثمّ سننشئ API باستخدام Python عبر الوحدات Flask وFastAPI. وأخيرًا، سنرى كيف نتواصل معه باستخدام بروتوكول HTTP عبر Curls أو عبر البرنامج Postman.

الملخّص كالتالي:

  1. API وRESTful API
  2. بروتوكول HTTP وCURL وPostman
  3. نموذج Data Science
  4. Flask
  5. FastAPI

API وRESTful API

API، اختصار لـ Application Programming Interface، أداة حوسبية تسمح بتغليف شيفرتك في خدمة سهلة وفعّالة للتواصل معها.
يمكن النظر إليها كخطوة لتحويل تطويراتك إلى صندوق أسود مع رموز اتّصال محدّدة مسبقًا تسمح لك بصفتك provider بكشفها بسهولة لـ clients أو consumers الذين قد يكونون مطوّري Front وBack-end ضمن فريقك.

مفهوم API

توجد العديد من APIs المجّانية (Weather, Flight search, Football،…) التي يمكن استكشافها على RapidAPI.
توجد أنواع عديدة من APIs:

  • Public أو Open APIs: لا توجد عليها قيود
  • Private أو Internal APIs: تُستخدم داخل الشركة نفسها
  • Partner APIs: تتطلّب ترخيصًا للوصول إليها

إذا كانت APIs تُتيح لتطبيقَين التواصل، فإنّ Web Service APIs، بدورها، تُتيح التفاعل بين جهازَين على شبكة معيّنة. وهي نظام يستخدم url على World Wide Web لتوفير الوصول إلى خدماته.
توجد أنواع عديدة من web service APIs مثل SOAP، JSON-RPC، XML-RPC،...إلخ. في هذا المقال، سنُركّز بشكل رئيسي على REST التي هي من نوع مختلف.
على عكس web service APIs الأخرى التي هي بروتوكولات، فإنّ REST، اختصار لـ REprentational State Transfer، هو مجموعة من 5 مبادئ معمارية رئيسية تجعل خدمة RESTful الويب خفيفة وفعّالة وقابلة للتوسعة وسهلة الاستخدام:

  • Client-server architecture: عندما يتواصل client مع server، يجب عليه إمّا قبول طلبه وإرسال استجابة أو رفضه وإبلاغه
  • Statelessness: تتمثّل في عدم تخزين أيّ معلومات على server. وتعني أيضًا أنّه يجب على client التأكّد من أنّ جميع البيانات اللازمة موجودة في الطلب
  • Cacheability: تُنفَّذ على جانب client من أجل إعادة استجابة أسرع عند إرسال طلب قديم
  • Layered system: هو القدرة على تركيب طبقات إضافية، على سبيل المثال، طبقة أمان أو موزّع حمولة دون التأثير على تبادلات client-server
  • Uniform interface: بكلمات بسيطة، هي استخدام URIs، اختصار لـ Uniform Resource Identifiers، لكشف بنية المستودع. مقترنة بطرق HTTP، تسمح بتبادلات XML أو JSON فعّالة مع server.
مفهوم API بتفصيل

بروتوكول HTTP وCURL وPostman

• بروتوكول HTTP

بمجرّد إنشاء web service API الخاصّ بك، ستحتاج إلى التواصل معه وهنا يأتي دور HTTP.
HTTP، اختصار لـ HyperText Transfer Protocol، هو بروتوكول اتّصال شبكي يُستخدم لتبادل البيانات على الويب. صُمّم لتسهيل الاتّصال بين خوادم الويب ومتصفّحات الويب مثل Google Chrome وSafari. إنه بروتوكول stateless يتبع معمارية client-server ممّا يجعل من السهل دمجه مع RESTful APIs.

بروتوكول HTTP

أدناه، بعض من أكثر الطرق استخدامًا في البروتوكول:

  • POST: ينشئ مَوْردًا على server
  • GET: يصل إلى مَوْرد على server
  • PUT: يُحدّث مَوْردًا على server
  • DELETE: يحذف مَوْردًا على server

• CURL

في كثير من الأحيان، عند العمل على آلة افتراضية، لا يكون لديك وصول إلّا إلى command line interface لأنّه لا توجد graphical interface وبالتالي لا يوجد متصفّح.
CURL، اختصار لـ Client URL Request Library وكان يُسمّى سابقًا httpget، هو أداة سطر أوامر تُستخدم للحصول على الموارد وإرسالها إلى server متصل بشبكة معيّنة. تدعم العديد من البروتوكولات بما في ذلك HTTP.
توجد العديد من options لـ curl لكن في هذا المقال، سنُركّز على واحد بشكل خاصّ لـ RESTful API الخاصّ بنا:

  • -X: يُحدّد طريقة HTTP المستخدمة عند التواصل مع server

• Postman

Postman هو برنامج أو منصّة تُبسّط تطوير واختبار APIs.
من خلال واجهته سهلة الاستخدام، يُتيح طريقة بسيطة جدًّا لإرسال الطلبات عن طريق:

  • اختيار method الخاصّة بـ HTTP
  • إدخال URL وport الذي يستمع عليه API
  • اختيار arguments، التي ستُحدّث طلب HTTP تلقائيًا
  • إرسال الطلب إلى API

يمكن تصوّر الاستجابة في أسفل الصفحة في قسم body.

تطبيق Postman

نموذج Data Science

للتوضيح، سننظر في dataset Iris الذي تتمثّل مهمّة ML فيه في تصنيف السوسن إلى ثلاث فئات (Setosa, Versicolour, and Virginica) باستخدام أربعة متغيّرات (Sepal Length, Sepal Width, Petal Length, and Petal Width).

سيتمّ تنزيل iris dataset من Sklearn وسنستخدم random forest classifier للتدريب

import numpy as np
import pandas as pd

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import confusion_matrix
from sklearn.metrics import accuracy_score
from sklearn.datasets import load_iris
from matplotlib import pyplot as plt
import joblib

%matplotlib inline
WEIGHTS_DIR="weights/"

iris = load_iris()

df=pd.DataFrame(iris.data, columns=iris.feature_names)
df["species"]=iris.target

X = df[iris.feature_names]
y = df['species']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=42)

clf = RandomForestClassifier(max_depth=2, random_state=42)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)

print("Confusion matrix: \n", confusion_matrix(y_test, y_pred))
print("Accuracy score: ", accuracy_score(y_test, y_pred, normalize=True) )

joblib.dump(clf, WEIGHTS_DIR+"clf_iris.joblib")

في الفقرات التالية سنُجري التنبّؤ على dataset التالية:

Predicition dataset

Flask

Flask هو وحدة Python طُوّرت لإنشاء APIs وكشف خدماتها على شبكة معيّنة. يمكن تثبيتها باستخدام سطر الأوامر التالي:

pip install flask

في الشيفرة التالية، سأنشئ flask API يستخدم النموذج المُدرَّب سابقًا للتنبّؤ بفئة السوسن بإعطاء المتغيّرات الأربعة كمدخلات.

• تهيئة API

#%%
from flask import Flask, request, jsonify
import pandas as pd
import joblib
import json
WEIGHTS_DIR = "weights/"
FLASK_API = Flask(__name__)

• تحميل النموذج

def get_iris_model():
    loaded_clf = joblib.load(WEIGHTS_DIR + "clf_iris.joblib")
    return loaded_clf
loaded_clf = get_iris_model()
def str_to_float_list(arg):
    arg = arg.split(",")
    arg = [float(x) for x in arg]
    return arg

• Routing

عند إنشاء API، تُستخدم Routes لكشف دوالها وخدماتها. في flask، تُضاف باستخدام decorators.

- predict_class_postman
ننشئ المسار الذي سنُجري عبره التنبّؤ. يُعيد هذا المسار استجابة json مع الفئة المقابلة لكلّ مجموعة من المتغيّرات. عند استخدام Postman، نستخرج المتغيّرات باستخدام بارامتر args الخاصّ بـ request.

#%%Postman
def get_params_postman(request):
    sep_length = str_to_float_list(request.args.get("sepLen"))
    sep_width = str_to_float_list(request.args.get("sepWid"))
    pet_length = str_to_float_list(request.args.get("petLen"))
    pet_width = str_to_float_list(request.args.get("petWid"))
    return (sep_length, sep_width, pet_length, pet_width)
@FLASK_API.route("/predict_class_postman", methods=["GET", "POST"])
def predict_class_postman():
    (sep_length, sep_width, pet_length, pet_width) = get_params_postman(request)
    new_row = pd.DataFrame(
        {
            "sepal length (cm)": [float(x) for x in sep_length],
            "sepal width (cm)": [float(x) for x in sep_width],
            "petal length (cm)": [float(x) for x in pet_length],
            "petal width (cm)": [float(x) for x in pet_width],
        }
    )
    y_pred = list(loaded_clf.predict(new_row))
    y_pred = [str(x) for x in y_pred]
    response = {"y_pred": ",".join(y_pred)}
    return jsonify(response)

- predict_class_curl
ننشئ مسارًا آخر هذه المرّة للتواصل مع أمر CURL. نستخرج المتغيّرات من سطر الأوامر باستخدام طريقة form.get من request المُرسَل.

#%%CURL
def get_params_curl(request):
    request_input = request.form.get("input")
    request_input = json.loads(request_input)
    sep_length = str_to_float_list(request_input["sepLen"])
    sep_width = str_to_float_list(request_input["sepWid"])
    pet_length = str_to_float_list(request_input["petLen"])
    pet_width = str_to_float_list(request_input["petWid"])
    return (sep_length, sep_width, pet_length, pet_width)
@FLASK_API.route("/predict_class_curl", methods=["GET", "POST"])
def predict_class_curl():
    (sep_length, sep_width, pet_length, pet_width) = get_params_curl(request)
    new_row = pd.DataFrame(
        {
            "sepal length (cm)": [float(x) for x in sep_length],
            "sepal width (cm)": [float(x) for x in sep_width],
            "petal length (cm)": [float(x) for x in pet_length],
            "petal width (cm)": [float(x) for x in pet_width],
        }
    )
    y_pred = list(loaded_clf.predict(new_row))
    y_pred = [str(x) for x in y_pred]
    response = {"y_pred": ",".join(y_pred)}
    return jsonify(response)

• بدء الخدمة

بمجرّد تعريف جميع العناصر أعلاه، نبدأ خدمة API بإضافة الشيفرة التالية:

#%%
if __name__ == "__main__":
    FLASK_API.debug = True
    FLASK_API.run(host="0.0.0.0", port="8080")
  • يمكن أن يكون debug mode مفيدًا لتصوّر التغييرات فورًا
  • يمكننا اختيار URL وport الذي يُكشف عليه API:

لإطلاق API، اكتب:

python flask_api.py

حيث flask_api.py هو الملفّ الذي يستضيف جميع الشيفرة المطوّرة أعلاه.

نحصل على الاستجابة التالية:

>>> * Serving Flask app "flask_api" (lazy loading)
>>> * Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
>>> * Debug mode: on
>>> * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
>>> * Restarting with fsevents reloader
>>> * Debugger is active!
>>> * Debugger PIN: 514-546-929

• Request وResponse

+ Postman
بمعطى HTTP request على Postman

localhost:8080/predict_class_postman?sepLen=1,5&sepWid=2,6&petLen=3,7&petWid=4,8

الاستجابة هي التالية:

{
  "y_pred": "1,2"
}
استدعاء API بواسطة Postman

+ CURL
نُطلق سطر الأوامر التالي باستخدام curl للتواصل مع API:

curl -F "input={\"sepLen\":\"1,5\",\"sepWid\":\"2,6\",\"petLen\":\"3,7\",\"petWid\":\"4,8\"}" -X POST "http://0.0.0.0:8080/predict_class_curl"

كما هو متوقَّع، نحصل على النتائج نفسها:

صورة بواسطة المؤلّف

HTTP codes: إذا كان الطلب صحيحًا يُعيد API رمز HTTP 200. توجد رموز أخرى مثل 4xx لخطأ client و5xx لخطأ server.

صورة بواسطة المؤلّف

يمكنك إيجاد شيفرة flask API في مستودع GitHub الخاصّ بي.

FastAPI

FastAPI هي وحدة Python أخرى تُتيح تطوير APIs.
يمكن تثبيتها باستخدام سطر الأوامر:

pip install fastapi

إنّها مشابهة جدًّا لـ Flask، لكنها أسرع، مع تغييرات طفيفة:

  • Query parameters من postman تُستخرج باستخدام request.query_params
  • Form parameters في curls تُحصَل عليها باستخدام eval(input) حيث input: str = Form(...)
#%%
import pandas as pd
import joblib
import json
from fastapi import FastAPI, Form, Request
import uvicorn

WEIGHTS_DIR = "weights/"
FASTAPI_API = FastAPI()

#%%
def get_iris_model():
    loaded_clf = joblib.load(WEIGHTS_DIR + "clf_iris.joblib")
    return loaded_clf


def str_to_float_list(arg):
    arg = arg.split(",")
    arg = [float(x) for x in arg]
    return arg


loaded_clf = get_iris_model()

#%%Postman
def get_params_postman(query_params):
    sep_length = str_to_float_list(query_params["sepLen"])
    sep_width = str_to_float_list(query_params["sepWid"])
    pet_length = str_to_float_list(query_params["petLen"])
    pet_width = str_to_float_list(query_params["petWid"])
    return (sep_length, sep_width, pet_length, pet_width)


@FASTAPI_API.post("/predict_class_postman")
def predict_class_postman(request: Request):
    query_params = dict(request.query_params)
    (sep_length, sep_width, pet_length, pet_width) = get_params_postman(query_params)
    new_row = pd.DataFrame(
        {
            "sepal length (cm)": [float(x) for x in sep_length],
            "sepal width (cm)": [float(x) for x in sep_width],
            "petal length (cm)": [float(x) for x in pet_length],
            "petal width (cm)": [float(x) for x in pet_width],
        }
    )
    y_pred = list(loaded_clf.predict(new_row))
    y_pred = [str(x) for x in y_pred]

    response = {"y_pred": ",".join(y_pred)}
    return response


#%%CURL
def get_params_curls(input_var):
    sep_length = str_to_float_list(input_var["sepLen"])
    sep_width = str_to_float_list(input_var["sepWid"])
    pet_length = str_to_float_list(input_var["petLen"])
    pet_width = str_to_float_list(input_var["petWid"])
    return (sep_length, sep_width, pet_length, pet_width)


@FASTAPI_API.post("/predict_class_curl")
def predict_class_curl(input: str = Form(...)):
    input_var = eval(input)
    (sep_length, sep_width, pet_length, pet_width) = get_params_curls(input_var)
    new_row = pd.DataFrame(
        {
            "sepal length (cm)": [float(x) for x in sep_length],
            "sepal width (cm)": [float(x) for x in sep_width],
            "petal length (cm)": [float(x) for x in pet_length],
            "petal width (cm)": [float(x) for x in pet_width],
        }
    )
    y_pred = list(loaded_clf.predict(new_row))
    y_pred = [str(x) for x in y_pred]

    response = {"y_pred": ",".join(y_pred)}
    return response


#%%
if __name__ == "__main__":
    uvicorn.run(FASTAPI_API, host="0.0.0.0", port=8080)

تُشغَّل FastAPI باستخدام Uvicorn. وهو خادم ASGI سريع جدًّا، مبني على uvloop وhttptools حيث uvloop هو بديل قائم على Cython لـ asyncio’s event loop يسمح بأن يكون أسرع 2-4 مرّات من event loop الافتراضي.
يمكن تثبيت Uvicorn باستخدام سطر الأوامر التالي:

pip install uvicorn

لإطلاق API، اكتب:

python fastapi_api.py

حيث fastapi_api.py هو الملفّ الذي يستضيف جميع الشيفرة المطوّرة أعلاه.

نحصل على الاستجابة التالية:

>>> INFO:     Started server process [50003]
>>> INFO:     Waiting for application startup.
>>> INFO:     Application startup complete.
>>> INFO:     Uvicorn running on http://0.0.0.0:8080 (Press CTRL+C to quit)

• Request وResponse

+ Postman
بمعطى طلب HTTP على Postman:

localhost:8080/predict_class_postman?sepLen=1,5&sepWid=2,6&petLen=3,7&petWid=4,8

الاستجابة هي التالية:

{  "y_pred": "1,2"}
صورة بواسطة المؤلّف

+ CURL
نُطلق سطر الأوامر التالي باستخدام curl للتواصل مع API:

curl -F "input={\"sepLen\":\"1,5\",\"sepWid\":\"2,6\",\"petLen\":\"3,7\",\"petWid\":\"4,8\"}" -X POST "http://0.0.0.0:8080/predict_class_curl"

كما هو متوقَّع، نحصل على النتائج نفسها مع رمز HTTP 200:

{
"y_pred": "1,2"
}

يمكنك إيجاد شيفرة FastAPI API في مستودع GitHub الخاصّ بي.

الخاتمة

APIs أدوات قويّة جدًّا تسمح لك بكشف عملك للخدمات وتسهيل التواصل معه. عند العمل في فريق من المطوّرين، يصبح إتقان هذه التقنيات حاسمًا لتقدّم المشروع.

ابقَ على تواصل

هل لديك سؤال؟ يسعدنا أن نسمع منك.