Machine Learning: نشر النموذج مع FastAPI و Docker

آخر تحديث: 2026-08-26

نموذج مدرب يجلس في Jupyter عديم القيمة — تتطلق قيمته فقط بمجرد نشره في الإنتاج.

1. ما ستتعلمه


2. قصة حقيقية من مهندس ML

(1) الألم: نموذج Notebook لا يمكنه خدمة العمل

درب Bob نموذج XGBoost بـ R²=0.89، لكنه عمل فقط داخل Jupyter Notebook. عندما سأل مدير المنتج، "هل يمكن للواجهة الأمامية استدعاء هذا؟" لم يكن لدى Bob أي فكرة عن كيفية تحويل النموذج إلى واجهة برمجة. الفجوة بين التدريب والنشر هي أكبر مشكلة "ميل أخير" في مشاريع ML.

(2) حل FastAPI + Docker

يلف FastAPI النموذج في واجهة برمجة REST، ويحوّله Docker إلى حاوية — يمكن لأي خدمة استدعاء التنبؤات عبر HTTP.

PYTHON
from fastapi import FastAPI
import joblib

app = FastAPI()
model = joblib.load("model.pkl")

@app.post("/predict")
def predict(features: PredictionInput):
    result = model.predict([features.dict()])
    return {"prediction": float(result[0])}

(3) النتيجة: ملايين الطلبات يوميًا بعد الإطلاق

بمجرد نشر Bob للنموذج باستخدام FastAPI + Docker، ظل زمن انتقال الواجهة أقل من 50 مللي ثانية مع التعامل مع أكثر من مليون طلب يوميًا، قابل للاستدعاء من الواجهة الأمامية ونظام CRM وأنظمة التوصية.


3. تسلسل النموذج

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

▶ مثال: تسلسل نموذج sklearn

PYTHON
import joblib
import pickle
from sklearn.ensemble import RandomForestRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
import numpy as np

# تدريب وحفظ النموذج
rng = np.random.default_rng(42)
X = rng.uniform(0, 100, (1000, 5))
y = 50 + 0.8 * X[:, 0] + 1.2 * X[:, 1] + rng.normal(0, 5, 1000)

pipe = Pipeline([
    ("scaler", StandardScaler()),
    ("model", RandomForestRegressor(n_estimators=100, random_state=42)),
])
pipe.fit(X, y)

# احفظ بـ joblib (موصى به لـ sklearn)
joblib.dump(pipe, "salespredict_model.joblib", compress=3)

# احفظ بـ pickle (بديل)
with open("salespredict_model.pkl", "wb") as f:
    pickle.dump(pipe, f)

# حمّل وتنبأ
loaded_model = joblib.load("salespredict_model.joblib")
sample = np.array([[50, 30, 20, 10, 5]])
prediction = loaded_model.predict(sample)
print(f"Prediction: {prediction[0]:.2f} thousand USD")

Output:

TEXT 📖 للعرض فقط
# Executed successfully
الطريقة الأفضل لـ المزايا العيوب
joblib sklearn/numpy فعّال للمصفوفات الكبيرة Python فقط
pickle أي كائن Python عالمي مخاطر أمنية، توافق الإصدار
torch.save PyTorch مرن (يمكن حفظ state_dict) PyTorch فقط
mlflow.sklearn sklearn الإصدار + البيانات الوصفية يتطلب MLflow
ONNX متعدد الأطر متعدد اللغات/المنصات تحويل معقد

▶ مثال: حفظ نموذج PyTorch

PYTHON
import torch
import torch.nn as nn

# احفظ state_dict للنموذج (موصى به)
class SimpleModel(nn.Module):
    def __init__(self):
        super().__init__()
        self.net = nn.Sequential(nn.Linear(5, 32), nn.ReLU(), nn.Linear(32, 1))

    def forward(self, x):
        return self.net(x)

model = SimpleModel()
torch.save(model.state_dict(), "pytorch_model.pt")

# حمّل
loaded = SimpleModel()
loaded.load_state_dict(torch.load("pytorch_model.pt", weights_only=True))
loaded.eval()

Output:

TEXT 📖 للعرض فقط
# Execution successful

4. خدمة النموذج بـ FastAPI

(1) أساسيات FastAPI

▶ مثال: واجهة برمجة تنبؤ كاملة

PYTHON
# File: app.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
import joblib
import numpy as np
import time

app = FastAPI(title="SalesPredict API", version="1.0.0")

# حمّل النموذج عند البدء
model = joblib.load("salespredict_model.joblib")

class PredictionInput(BaseModel):
    ad_spend_k: float = Field(..., ge=0, description="Ad spend in thousand USD")
    traffic_k: float = Field(..., ge=0, description="Traffic in thousands")
    category_electronics: float = Field(0, ge=0, le=1)
    category_clothing: float = Field(0, ge=0, le=1)
    is_promotion: float = Field(0, ge=0, le=1)

    model_config = {"json_schema_extra": {
        "example": {"ad_spend_k": 50, "traffic_k": 300,
                     "category_electronics": 1, "category_clothing": 0, "is_promotion": 1}
    }}

class PredictionOutput(BaseModel):
    predicted_revenue_k: float
    latency_ms: float

@app.get("/health")
def health_check():
    return {"status": "healthy", "model_loaded": model is not None}

@app.post("/predict", response_model=PredictionOutput)
def predict(input_data: PredictionInput):
    start = time.time()
    try:
        features = np.array([[input_data.ad_spend_k, input_data.traffic_k,
                               input_data.category_electronics,
                               input_data.category_clothing, input_data.is_promotion]])
        prediction = model.predict(features)[0]
        latency = (time.time() - start) * 1000
        return PredictionOutput(predicted_revenue_k=round(float(prediction), 2),
                                 latency_ms=round(latency, 2))
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/predict_batch")
def predict_batch(inputs: list[PredictionInput]):
    features = np.array([[d.ad_spend_k, d.traffic_k, d.category_electronics,
                           d.category_clothing, d.is_promotion] for d in inputs])
    predictions = model.predict(features)
    return {"predictions": [round(float(p), 2) for p in predictions]}

Output:

TEXT 📖 للعرض فقط
# Execution successful

(2) تشغيل خدمة FastAPI

BASH
# تثبيت التبعيات
pip install fastapi uvicorn joblib scikit-learn

# تشغيل خادم API
uvicorn app:app --host 0.0.0.0 --port 8000 --reload

# اختبر بـ curl
curl -X POST http://localhost:8000/predict \
  -H "Content-Type: application/json" \
  -d '{"ad_spend_k": 50, "traffic_k": 300, "category_electronics": 1, "category_clothing": 0, "is_promotion": 1}'

# الوصول إلى Swagger UI: http://localhost:8000/docs
ميزة FastAPI الوصف
التحقق من Pydantic يتحقق تلقائيًا من أنواع ونطاقات الإدخال
Swagger UI يولد تلقائيًا وثائق تفاعلية (/docs)
تلميحات الأنواع يولد تلقائيًا نماذج الاستجابة
الدعم غير المتزامن async/await لتزامن عالٍ
معالجة الاستثناءات أكواد خطأ قياسية عبر HTTPException

5. حاوية Docker

(1) كتابة Dockerfile

▶ مثال: صورة Docker الخاصة بـ SalesPredict

DOCKERFILE
# Stage 1: Build dependencies
FROM python:3.11-slim AS builder

WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# Stage 2: Runtime (smaller image)
FROM python:3.11-slim

WORKDIR /app

# Copy installed packages from builder
COPY --from=builder /install /usr/local

# Copy application code and model
COPY app.py .
COPY salespredict_model.joblib .

# Non-root user for security
RUN useradd -m appuser
USER appuser

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s \
  CMD curl -f http://localhost:8000/health || exit 1

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
TEXT 📖 للعرض فقط
# requirements.txt
fastapi==0.109.0
uvicorn==0.27.0
joblib==1.3.2
scikit-learn==1.4.0
numpy==1.26.4
pydantic==2.5.0

(2) أوامر Docker

BASH
# بناء الصورة
docker build -t salespredict-api:latest .

# تشغيل الحاوية
docker run -d -p 8000:8000 --name salespredict salespredict-api:latest

# اختبار
curl http://localhost:8000/health

# عرض السجلات
docker logs salespredict

# إيقاف وإزالة
docker stop salespredict && docker rm salespredict
تحسين Dockerfile التأثير
بناء متعدد المراحل يصغر الصورة من 1.5 جيجابايت إلى 200 ميجابايت
صورة أساسية نحيفة يزيل حزم النظام غير الضرورية
.dockerignore يستبعد الملفات الكبيرة مثل .git/data
مستخدم غير جذر تقوية الأمان
HEALTHCHECK فحوصات صحة الحاوية

6. تنسيق docker-compose

تربط بنية النشر الكاملة جميع المكونات معًا — خدمة API وذاكرة التخزين المؤقت وموازن التحميل والمراقبة تشكل خط أنابيب شامل من البداية إلى النهاية:

100%
graph TB
    CLIENT[العميل / الواجهة الأمامية] --> NGINX[Nginx<br/>تحديد المعدل + LB]
    NGINX --> API1[عامل FastAPI 1]
    NGINX --> API2[عامل FastAPI 2]
    API1 --> REDIS[(ذاكرة التخزين المؤقت Redis<br/>LRU 256MB)]
    API2 --> REDIS
    API1 --> MODEL[ملف النموذج<br/>.joblib]
    API2 --> MODEL
    PROM[Prometheus<br/>المقاييس] --> API1
    PROM --> API2
    GRAF[Grafana<br/>لوحة المعلومات] --> PROM

▶ مثال: بنية نشر إنتاج كاملة

YAML
# docker-compose.yml
version: "3.8"

services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      - REDIS_URL=redis://redis:6379
    depends_on:
      - redis
    deploy:
      replicas: 2
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf
    depends_on:
      - api
    restart: unless-stopped

volumes:
  redis_data:
TEXT 📖 للعرض فقط
# nginx.conf (موازن تحميل مبسط)
upstream api_servers {
    server api:8000;
}

server {
    listen 80;
    location / {
        proxy_pass http://api_servers;
        proxy_set_header Host $host;
    }
}

▶ مثال: API + ذاكرة التخزين المؤقت Redis

PYTHON
# app.py محسن مع تخزين Redis مؤقت
from fastapi import FastAPI
from pydantic import BaseModel
import joblib
import numpy as np
import hashlib
import json

app = FastAPI(title="SalesPredict API with Cache")
model = joblib.load("salespredict_model.joblib")

# Redis cache (conceptual)
# import redis
# redis_client = redis.from_url(os.getenv("REDIS_URL", "redis://localhost:6379"))

class PredictionInput(BaseModel):
    ad_spend_k: float
    traffic_k: float
    category_electronics: float = 0
    category_clothing: float = 0
    is_promotion: float = 0

def get_cache_key(input_data: PredictionInput) -> str:
    data_str = json.dumps(input_data.model_dump(), sort_keys=True)
    return f"pred:{hashlib.md5(data_str.encode()).hexdigest()}"

@app.post("/predict")
def predict(input_data: PredictionInput):
    cache_key = get_cache_key(input_data)

    # تحقق من ذاكرة التخزين المؤقت أولًا
    # cached = redis_client.get(cache_key)
    # if cached:
    #     return json.loads(cached)

    features = np.array([[input_data.ad_spend_k, input_data.traffic_k,
                           input_data.category_electronics,
                           input_data.category_clothing, input_data.is_promotion]])
    prediction = float(model.predict(features)[0])
    result = {"predicted_revenue_k": round(prediction, 2)}

    # خزن مؤقتًا لمدة 5 دقائق
    # redis_client.setex(cache_key, 300, json.dumps(result))

    return result

Output:

TEXT 📖 للعرض فقط
# Execution successful
المكون الدور اختيار التقنية
خدمة API استدلال النموذج FastAPI + Uvicorn
ذاكرة التخزين المؤقت تخزين التنبؤات الساخنة مؤقتًا Redis (TTL 5 دقائق)
موازن التحميل توزيع الطلبات Nginx
تنسيق الحاويات إدارة الخدمة docker-compose
فحوصات الصحة اكتشاف الفشل /health + HEALTHCHECK

❓ أسئلة شائعة

س أيهما أفضل، pickle أم joblib؟
ج استخدم joblib لنماذج sklearn (يضغط المصفوفات numpy الكبيرة بكفاءة أكبر)؛ استخدم pickle لكائنات Python العامة. كلاهما يحمل مخاطر أمنية (ملف pickle غير موثوق يمكنه تنفيذ كود ضار)، لذلك MLflow أو ONNX أكثر أمانًا للإنتاج.
س هل أستخدم FastAPI أم Flask؟
ج استخدم FastAPI للمشاريع الجديدة - الوثائق التلقائية (Swagger)، والتحقق من الأنواع (Pydantic)، والدعم غير المتزامن، والأداء الأفضل. Flask أكثر نضجًا، لكن تجربة تطوير API لا تضاهي FastAPI.
س ماذا لو كانت صورة Docker الخاصة بي كبيرة جدًا؟
ج ثلاث حيل — 1) البناء متعدد المراحل (مرحلة البناء لا تنتهي في الصورة النهائية)؛ 2) استخدام صور أساسية نحيفة / alpine؛ 3) استخدام .dockerignore لاستبعاد .git/data وما شابه ذلك.
س كيف أحدث النموذج دون توقف؟
ج نهجان — 1) النشر الأزرق-الأخضر (التبديل بين الإصدارين القديم والجديد)؛ 2) التحديثات المتدرجة (تحديث تدريجي لـ docker-compose). اقترن هذا مع MLflow Model Registry لإدارة الإصدارات.
س كيف أحسن زمن انتقال API؟
ج أربع طبقات من التحسين — 1) تخزين الطلبات الساخنة مؤقتًا مع Redis؛ 2) التنبؤات الدفعية لتقليل الحمل الزائد؛ 3) تشغيل عمال متعددين بالتوازي (عمال Uvicorn)؛ 4) تكميم النموذج (تقليص حجمه).
س كيف أحد من طلبات API؟
ج استخدم مكتبة slowapi لتحديد المعدل — limiter = Limiter(key_func=get_remote_address)، مثلًا تحديد الطلبات بـ 100 في الدقيقة. هذا يمنع الإساءة والحمل الزائد.

📖 ملخص

📝 تمارين

  1. أساسي (الصعوبة ⭐): درب نموذج sklearn، واحفظه بـ joblib، ثم حمّله في نص Python منفصل وقم بتنبؤ. تلميح: joblib.dump/load.
  2. متوسط (الصعوبة ⭐⭐): أنشئ نقطة نهاية /predict باستخدام FastAPI، بما في ذلك التحقق من إدخال Pydantic وفحص /health، ثم شغّلها باستخدام uvicorn واختبرها بـ curl. تلميح: راجع كود API الكامل في القسم 4.
  3. تحدٍّ (الصعوبة ⭐⭐⭐): اكتب Dockerfile (بناء متعدد المراحل) + docker-compose.yml (API + Redis + Nginx)، وابنِ الصورة وشغّل مكدس الخدمة الكامل، ثم تحقق من موازنة التحميل والتخزين المؤقت. تلميح: راجع ملفات التكوين في الأقسام 5-6.

← الدرس السابق: مقدمة في MLOps | الدرس التالي: اختبار A/B →

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%