كيفية نشر نموذج معالجة لغة طبيعية باستخدام FastAPI

دقائق القراءة: 10

مقدمة: لماذا يُعد نشر نموذج NLP خطوة حاسمة؟

إذا كنت تعمل في مجال معالجة اللغة الطبيعية، فإن بناء النموذج وحده لا يكفي. القيمة الحقيقية تظهر عندما يتحول النموذج من تجربة محلية إلى خدمة يمكن للتطبيقات والأنظمة الأخرى استخدامها فعلياً داخل بيئة الإنتاج. هذه العملية تُعرف باسم نشر النموذج، وهي تعني دمج النموذج في نظام قائم ليستقبل مدخلات ويُرجع تنبؤات قابلة للاستخدام في اتخاذ القرار.

توجد عدة خيارات شائعة لنشر نماذج NLP مثل Flask وDjango وBottle، لكن FastAPI أصبح خياراً مفضلاً لدى كثير من المطورين بفضل سرعته وسهولة استخدامه ودعمه التوثيق التفاعلي تلقائياً.

في هذا الدليل العملي ستتعرف على كيفية:

  • بناء نموذج بسيط لتصنيف مراجعات أفلام IMDB إلى إيجابية أو سلبية.
  • فهم إطار العمل FastAPI وطريقة تثبيته.
  • نشر النموذج على هيئة واجهة REST API.
  • استخدام النموذج المنشور من داخل أي تطبيق مكتوب بلغة Python.

نشر نموذج معالجة اللغة الطبيعية باستخدام FastAPI وتحويله إلى واجهة برمجية قابلة للاستخدام

بناء نموذج NLP لتصنيف مراجعات الأفلام

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

استيراد الحزم الأساسية

نبدأ باستيراد الحزم اللازمة لتحميل البيانات، وتنظيف النصوص، وبناء نموذج التصنيف، ثم حفظه لاستخدامه لاحقاً أثناء النشر.

# import important modules
import numpy as np
import pandas as pd

# sklearn modules
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.naive_bayes import MultinomialNB # classifier
from sklearn.metrics import (
 accuracy_score,
 classification_report,
 plot_confusion_matrix,
)
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer

# text preprocessing modules
from string import punctuation

# text preprocessing modules
from nltk.tokenize import word_tokenize
import nltk
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
import re #regular expression

# Download dependency
for dependency in (
 "brown",
 "names",
 "wordnet",
 "averaged_perceptron_tagger",
 "universal_tagset",
):
 nltk.download(dependency)

import warnings
warnings.filterwarnings("ignore")

# seeding
np.random.seed(123

تحميل مجموعة البيانات

يتم تحميل الملف من مجلد البيانات باستخدام pandas:

# load data
data = pd.read_csv("../data/labeledTrainData.tsv", sep='\t')

ويمكن عرض أول الصفوف للتأكد من بنية البيانات:

# show top five rows of data
data.head()

معاينة أول صفوف من مجموعة بيانات IMDB المستخدمة في تدريب نموذج تحليل المشاعر

تضم مجموعة البيانات ثلاثة أعمدة رئيسية:

  • id: المعرّف الخاص بكل مراجعة.
  • sentiment: التصنيف، حيث تمثل 1 مراجعة إيجابية و0 مراجعة سلبية.
  • review: نص المراجعة نفسه.

فحص حجم البيانات والقيم المفقودة

# check the shape of the data
data.shape
(25000, 3)

هذا يعني أن لدينا 25,000 مراجعة جاهزة للتدريب والتقييم.

# check missing values in data
data.isnull().sum()

id           0
sentiment    0
review       0
dtype: int64

لا تحتوي البيانات على قيم مفقودة، وهي نقطة مهمة قبل بدء أي معالجة.

تحليل توزيع الفئات

قبل التدريب، من الجيد التحقق من توازن الفئات باستخدام الدالة value_counts():

# evalute news sentiment distribution
data.sentiment.value_counts()

1    12500
0    12500
Name: sentiment, dtype: int64

نلاحظ أن البيانات متوازنة تماماً بين المراجعات الإيجابية والسلبية، وهذا يجعل مقياس accuracy مناسباً للتقييم في هذه الحالة.

معالجة النصوص قبل التدريب

النصوص الخام عادةً تحتوي على روابط ورموز وعلامات ترقيم وكلمات عامة كثيرة لا تفيد النموذج. لذلك سننفذ مرحلة تنظيف للنصوص لتحسين جودة المدخلات.

دالة تنظيف النصوص

الدالة text_cleaning() ستتولى إزالة العناصر غير الضرورية، ثم تنفيذ عملية lemmatization لتحويل الكلمات إلى صيغها الأساسية.

stop_words = stopwords.words('english')

def text_cleaning(text, remove_stop_words=True, lemmatize_words=True):
    # Clean the text, with the option to remove stop_words and to lemmatize word

    # Clean the text
    text = re.sub(r"[^A-Za-z0-9]", " ", text)
    text = re.sub(r"\'s", " ", text)
    text = re.sub(r'http\S+', ' link ', text)
    text = re.sub(r'\b\d+(?:\.\d+)?\s+', '', text)  # remove numbers

    # Remove punctuation from text
    text = ''.join([c for c in text if c not in punctuation])

    # Optionally, remove stop words
    if remove_stop_words:
        text = text.split()
        text = [w for w in text if not w in stop_words]
        text = " ".join(text)

    # Optionally, shorten words to their stems
    if lemmatize_words:
        text = text.split()
        lemmatizer = WordNetLemmatizer()
        lemmatized_words = [lemmatizer.lemmatize(word) for word in text]
        text = " ".join(lemmatized_words)

    # Return a list of words
    return (text)

بعد ذلك نطبق الدالة على عمود المراجعات:

#clean the review
data["cleaned_review"] = data["review"].apply(text_cleaning)

تحديد المتغيرات وتقسيم البيانات

سنستخدم العمود cleaned_review كمدخلات تدريب، بينما سيكون العمود sentiment هو الهدف المطلوب التنبؤ به.

#split features and target from data
X = data["cleaned_review"]
y = data.sentiment.values

ثم نقسم البيانات إلى جزء للتدريب وآخر للاختبار بنسبة 15% للتحقق من الأداء:

# split data into train and validate
X_train, X_valid, y_train, y_valid = train_test_split(
    X,
    y,
    test_size=0.15,
    random_state=42,
    shuffle=True,
    stratify=y,
)

إنشاء نموذج تصنيف المشاعر

سنستخدم خوارزمية Multinomial Naive Bayes، وهي من أكثر الخوارزميات انتشاراً في تصنيف النصوص بسبب بساطتها وكفاءتها العالية مع الميزات النصية.

تحويل النص إلى تمثيل رقمي

قبل تدريب النموذج، يجب تحويل النصوص إلى أرقام يمكن للخوارزمية التعامل معها. سنعتمد على TfidfVectorizer لتحويل مجموعة الوثائق النصية إلى مصفوفة من ميزات TF-IDF.

ولتنفيذ جميع الخطوات بسلاسة، سنضع المعالجة والتصنيف داخل كائن Pipeline.

# Create a classifier in pipeline
sentiment_classifier = Pipeline(steps=[
    ('pre_processing', TfidfVectorizer(lowercase=False)),
    ('naive_bayes', MultinomialNB())
])

تدريب النموذج وتقييمه

# train the sentiment classifier
sentiment_classifier.fit(X_train, y_train)
# test model performance on valid data
y_preds = sentiment_classifier.predict(X_valid)
accuracy_score(y_valid, y_preds)
0.8629333333333333

حقق النموذج دقة تقارب 86.29%، وهي نتيجة جيدة بالنسبة لنموذج أولي بسيط لتصنيف المشاعر.

حفظ النموذج لاستخدامه في النشر

بعد الانتهاء من التدريب، نحفظ النموذج باستخدام مكتبة joblib حتى نتمكن من تحميله لاحقاً داخل تطبيق FastAPI.

#save model
import joblib
joblib.dump(sentiment_classifier, '../models/sentiment_model_pipeline.pkl')

ما هو FastAPI ولماذا يُستخدم؟

FastAPI هو إطار عمل حديث وسريع لبناء واجهات برمجية باستخدام Python. يتميز بأداء مرتفع، وصياغة واضحة، وقدرته على إنشاء توثيق تفاعلي تلقائي دون الحاجة إلى إعدادات إضافية معقدة.

إطار FastAPI لتطوير واجهات برمجية سريعة وعصرية في بايثون

يعتمد FastAPI على مكتبتين أساسيتين:

  • Starlette لإدارة الجوانب المتعلقة بالويب والطلبات.
  • Pydantic للتحقق من البيانات ومعالجتها.

كما يُعد خياراً ممتازاً عند الحاجة إلى الأداء الجيد ودعم العمليات غير المتزامنة async.

تثبيت FastAPI وخادم التشغيل

لتثبيت FastAPI، نفّذ الأمر التالي:

pip install fastapi

وستحتاج أيضاً إلى خادم ASGI مثل uvicorn لتشغيل التطبيق:

pip install uvicorn

نشر نموذج NLP باستخدام FastAPI

سنحوّل النموذج المدرّب إلى واجهة API يمكن استدعاؤها عبر طلبات HTTP. سنضع الكود في ملف باسم main.py.

استيراد الحزم المطلوبة

# text preprocessing modules
from string import punctuation

# text preprocessing modules
from nltk.tokenize import word_tokenize
import nltk
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
import re # regular expression
import os
from os.path import dirname, join, realpath
import joblib
import uvicorn
from fastapi import FastAPI

تهيئة تطبيق FastAPI

app = FastAPI(
    title="Sentiment Model API",
    description="A simple API that use NLP model to predict the sentiment of the movie's reviews",
    version="0.1",
)

هنا قمنا بتخصيص:

  • عنوان الواجهة البرمجية.
  • وصف مختصر للخدمة.
  • رقم الإصدار.

تحميل النموذج المحفوظ

# load the sentiment model
with open(
    join(dirname(realpath(__file__)), "models/sentiment_model_pipeline.pkl"),
    "rb"
) as f:
    model = joblib.load(f)

إعادة استخدام دالة تنظيف النصوص

يجب أن تكون خطوات تنظيف النص وقت التنبؤ مماثلة لما تم أثناء التدريب، حتى لا تختلف طبيعة المدخلات ويضعف الأداء.

def text_cleaning(text, remove_stop_words=True, lemmatize_words=True):
    # Clean the text, with the option to remove stop_words and to lemmatize word

    # Clean the text
    text = re.sub(r"[^A-Za-z0-9]", " ", text)
    text = re.sub(r"\'s", " ", text)
    text = re.sub(r"http\S+", " link ", text)
    text = re.sub(r"\b\d+(?:\.\d+)?\s+", "", text)  # remove numbers

    # Remove punctuation from text
    text = "".join([c for c in text if c not in punctuation])

    # Optionally, remove stop words
    if remove_stop_words:
        # load stopwords
        stop_words = stopwords.words("english")
        text = text.split()
        text = [w for w in text if not w in stop_words]
        text = " ".join(text)

    # Optionally, shorten words to their stems
    if lemmatize_words:
        text = text.split()
        lemmatizer = WordNetLemmatizer()
        lemmatized_words = [lemmatizer.lemmatize(word) for word in text]
        text = " ".join(lemmatized_words)

    # Return a list of words
    return text

إنشاء نقطة التنبؤ Endpoint

سنضيف مساراً باسم /predict-review باستخدام الطلب من نوع GET.

@app.get("/predict-review")

نقطة النهاية هي عنوان الاستقبال الذي تتواصل عبره التطبيقات مع الواجهة البرمجية. عند استدعاء هذا المسار، سيتم تنفيذ دالة التنبؤ.

@app.get("/predict-review")
def predict_sentiment(review: str):
    """
    A simple function that receive a review content and predict the sentiment of the content.
    :param review:
    :return: prediction, probabilities
    """
    # clean the review
    cleaned_review = text_cleaning(review)

    # perform prediction
    prediction = model.predict([cleaned_review])
    output = int(prediction[0])
    probas = model.predict_proba([cleaned_review])
    output_probability = "{:.2f}".format(float(probas[:, output]))

    # output dictionary
    sentiments = {0: "Negative", 1: "Positive"}

    # show results
    result = {
        "prediction": sentiments[output],
        "Probability": output_probability
    }
    return result

تقوم هذه الدالة بالخطوات التالية:

  1. استقبال نص المراجعة من المستخدم.
  2. تنظيف النص عبر الدالة text_cleaning().
  3. تمرير النص المنظف إلى النموذج.
  4. استخراج التصنيف النهائي واحتماله.
  5. إرجاع النتيجة بصيغة JSON.

الملف الكامل main.py

# text preprocessing modules
from string import punctuation

# text preprocessing modules
from nltk.tokenize import word_tokenize
import nltk
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
import re # regular expression
import os
from os.path import dirname, join, realpath
import joblib
import uvicorn
from fastapi import FastAPI

app = FastAPI(
    title="Sentiment Model API",
    description="A simple API that use NLP model to predict the sentiment of the movie's reviews",
    version="0.1",
)

# load the sentiment model
with open(join(dirname(realpath(__file__)), "models/sentiment_model_pipeline.pkl"), "rb") as f:
    model = joblib.load(f)

# cleaning the data
def text_cleaning(text, remove_stop_words=True, lemmatize_words=True):
    # Clean the text, with the option to remove stop_words and to lemmatize word

    # Clean the text
    text = re.sub(r"[^A-Za-z0-9]", " ", text)
    text = re.sub(r"\'s", " ", text)
    text = re.sub(r"http\S+", " link ", text)
    text = re.sub(r"\b\d+(?:\.\d+)?\s+", "", text)  # remove numbers

    # Remove punctuation from text
    text = "".join([c for c in text if c not in punctuation])

    # Optionally, remove stop words
    if remove_stop_words:
        # load stopwords
        stop_words = stopwords.words("english")
        text = text.split()
        text = [w for w in text if not w in stop_words]
        text = " ".join(text)

    # Optionally, shorten words to their stems
    if lemmatize_words:
        text = text.split()
        lemmatizer = WordNetLemmatizer()
        lemmatized_words = [lemmatizer.lemmatize(word) for word in text]
        text = " ".join(lemmatized_words)

    # Return a list of words
    return text

@app.get("/predict-review")
def predict_sentiment(review: str):
    """
    A simple function that receive a review content and predict the sentiment of the content.
    :param review:
    :return: prediction, probabilities
    """
    # clean the review
    cleaned_review = text_cleaning(review)

    # perform prediction
    prediction = model.predict([cleaned_review])
    output = int(prediction[0])
    probas = model.predict_proba([cleaned_review])
    output_probability = "{:.2f}".format(float(probas[:, output]))

    # output dictionary
    sentiments = {0: "Negative", 1: "Positive"}

    # show results
    result = {
        "prediction": sentiments[output],
        "Probability": output_probability
    }
    return result

تشغيل واجهة API

لتشغيل التطبيق محلياً استخدم الأمر التالي:

uvicorn main:app --reload

معنى الأجزاء السابقة هو:

  • main: اسم الملف main.py.
  • app: كائن التطبيق الذي أُنشئ بالسطر app = FastAPI().
  • --reload: إعادة تشغيل الخادم تلقائياً عند تعديل الكود، وهو مفيد أثناء التطوير.

تشغيل تطبيق FastAPI محلياً باستخدام uvicorn من سطر الأوامر

الوصول إلى التوثيق التفاعلي

يوفر FastAPI صفحة توثيق تفاعلية تلقائية. بعد تشغيل التطبيق، انتقل إلى الرابط http://127.0.0.1:8000/docs من المتصفح.

واجهة التوثيق التفاعلي التلقائي في FastAPI لعرض المسارات واختبارها

ستظهر لك معلومات الواجهة، مثل الاسم والوصف والإصدار، إضافة إلى قائمة المسارات المتاحة للتفاعل المباشر.

اختبار نقطة التنبؤ من المتصفح

لاختبار الخدمة:

  1. افتح المسار predict-review.
  2. اضغط زر Try it out.
  3. أدخل نص مراجعة فيلم في الحقل المطلوب.
  4. اضغط Execute للحصول على التنبؤ.

اختبار مسار predict-review داخل صفحة توثيق FastAPI التفاعلية

في المثال التطبيقي، أُدخلت مراجعة إيجابية لفيلم Zack Snyder’s Justice League، ثم أُرسلت إلى النموذج لتحليل المشاعر.

تنفيذ طلب تنبؤ بمراجعة فيلم من خلال واجهة FastAPI التفاعلية

أظهرت النتيجة أن النموذج صنّف المراجعة على أنها إيجابية باحتمال 0.70.

نتيجة تصنيف مراجعة فيلم إلى مشاعر إيجابية عبر واجهة FastAPI

استخدام نموذج NLP المنشور داخل أي تطبيق Python

بعد نشر النموذج، يمكنك استدعاؤه من أي تطبيق خارجي. سنستخدم مكتبة requests لإرسال طلب HTTP إلى الواجهة البرمجية.

تثبيت مكتبة requests

pip install requests

إنشاء ملف عميل بسيط

أنشئ ملفاً باسم python_app.py، ثم استورد المكتبة:

import requests as r

أضف نص مراجعة ترغب في تحليلها:

# add review
review = "This movie was exactly what I wanted in a Godzilla vs Kong movie. It's big loud, brash and dumb, in the best ways possible. It also has a heart in a the form of Jia (Kaylee Hottle) and a superbly expressionful Kong. The scenes of him in the hollow world are especially impactful and beautifully shot/animated. Kong really is the emotional core of the film (with Godzilla more of an indifferent force of nature), and is done so well he may even convert a few members of Team Godzilla."

مرّر النص إلى الطلب عبر معلمة review:

keys = {
    "review": review
}

ثم أرسل الطلب إلى المسار المنشور:

prediction = r.get("http://127.0.0.1:8000/predict-review/", params=keys)

وأخيراً اعرض النتيجة:

results = prediction.json()
print(results["prediction"])
print(results["Probability"])

ستظهر نتيجة مشابهة لما يلي:

Positive
0.54

أفضل ممارسات مهمة قبل النشر الفعلي

رغم أن المثال السابق مناسب للتعلّم وبناء نموذج أولي، فإن النشر في بيئة إنتاج حقيقية يتطلب الانتباه إلى بعض الجوانب الإضافية:

  • التحقق من صحة المدخلات وتحديد طول المراجعة المقبول.
  • إضافة معالجة للأخطاء في حال فشل تحميل النموذج أو تعذر التنبؤ.
  • تثبيت إصدارات الحزم داخل ملف requirements.txt.
  • التأكد من توحيد خطوات المعالجة بين التدريب والإنتاج.
  • التفكير في استخدام طلبات POST بدلاً من GET عند التعامل مع نصوص طويلة.
  • مراقبة الأداء والزمن المستغرق في التنبؤ عند زيادة عدد الطلبات.

الخلاصة التقنية

يُعد الجمع بين نموذج تصنيف نصوص مبني باستخدام scikit-learn وإطار FastAPI خياراً عملياً وسريعاً لتحويل النماذج التجريبية إلى خدمات قابلة للاستهلاك برمجياً. الميزة الأهم هنا ليست فقط سهولة كتابة الكود، بل القدرة على إنشاء واجهة موثقة تلقائياً يمكن اختبارها فوراً. وإذا أردت تطوير هذا المشروع لاحقاً، فابدأ بتحسين جودة المعالجة النصية، واعتماد نماذج أقوى، ثم تغليف الخدمة داخل بيئة إنتاج آمنة وقابلة للتوسع.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *