Machine Learning: نشر النموذج مع FastAPI و Docker
آخر تحديث: 2026-08-26
نموذج مدرب يجلس في Jupyter عديم القيمة — تتطلق قيمته فقط بمجرد نشره في الإنتاج.
1. ما ستتعلمه
- خدمة النموذج بـ FastAPI: التحقق من Pydantic، نقاط نهاية التنبؤ غير المتزامنة، ووثائق Swagger المولدة تلقائيًا
- تسلسل النموذج: حفظ نماذج sklearn بـ joblib/pickle ونماذج PyTorch بـ torch.save
- حاوية Docker: كتابة Dockerfiles، والبناء متعدد المراحل، وتحسين الصورة
- تنسيق docker-compose: خدمة النموذج + ذاكرة التخزين المؤقت Redis + موازنة تحميل Nginx
- واجهة برمجة التنبؤ لـ Bob: تصميم نقطة نهاية POST /predict بزمن انتقال أقل من 50 مللي ثانية لكل تنبؤ
2. قصة حقيقية من مهندس ML
(1) الألم: نموذج Notebook لا يمكنه خدمة العمل
درب Bob نموذج XGBoost بـ R²=0.89، لكنه عمل فقط داخل Jupyter Notebook. عندما سأل مدير المنتج، "هل يمكن للواجهة الأمامية استدعاء هذا؟" لم يكن لدى Bob أي فكرة عن كيفية تحويل النموذج إلى واجهة برمجة. الفجوة بين التدريب والنشر هي أكبر مشكلة "ميل أخير" في مشاريع ML.
(2) حل FastAPI + Docker
يلف FastAPI النموذج في واجهة برمجة REST، ويحوّله Docker إلى حاوية — يمكن لأي خدمة استدعاء التنبؤات عبر HTTP.
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
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:
# Executed successfully
| الطريقة | الأفضل لـ | المزايا | العيوب |
|---|---|---|---|
| joblib | sklearn/numpy | فعّال للمصفوفات الكبيرة | Python فقط |
| pickle | أي كائن Python | عالمي | مخاطر أمنية، توافق الإصدار |
| torch.save | PyTorch | مرن (يمكن حفظ state_dict) | PyTorch فقط |
| mlflow.sklearn | sklearn | الإصدار + البيانات الوصفية | يتطلب MLflow |
| ONNX | متعدد الأطر | متعدد اللغات/المنصات | تحويل معقد |
▶ مثال: حفظ نموذج PyTorch
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:
# Execution successful
4. خدمة النموذج بـ FastAPI
(1) أساسيات FastAPI
▶ مثال: واجهة برمجة تنبؤ كاملة
# 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:
# Execution successful
(2) تشغيل خدمة FastAPI
# تثبيت التبعيات
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
# 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"]
# 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
# بناء الصورة
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 وذاكرة التخزين المؤقت وموازن التحميل والمراقبة تشكل خط أنابيب شامل من البداية إلى النهاية:
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
▶ مثال: بنية نشر إنتاج كاملة
# 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:
# nginx.conf (موازن تحميل مبسط)
upstream api_servers {
server api:8000;
}
server {
listen 80;
location / {
proxy_pass http://api_servers;
proxy_set_header Host $host;
}
}
▶ مثال: API + ذاكرة التخزين المؤقت Redis
# 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:
# Execution successful
| المكون | الدور | اختيار التقنية |
|---|---|---|
| خدمة API | استدلال النموذج | FastAPI + Uvicorn |
| ذاكرة التخزين المؤقت | تخزين التنبؤات الساخنة مؤقتًا | Redis (TTL 5 دقائق) |
| موازن التحميل | توزيع الطلبات | Nginx |
| تنسيق الحاويات | إدارة الخدمة | docker-compose |
| فحوصات الصحة | اكتشاف الفشل | /health + HEALTHCHECK |
❓ أسئلة شائعة
limiter = Limiter(key_func=get_remote_address)، مثلًا تحديد الطلبات بـ 100 في الدقيقة. هذا يمنع الإساءة والحمل الزائد.📖 ملخص
- تسلسل النموذج: استخدم joblib لـ sklearn، و torch.save (state_dict) لـ PyTorch، ويوصى بـ MLflow للإنتاج
- يوفر FastAPI واجهة REST: يتحقق Pydantic من المدخلات، ويولد Swagger الوثائق تلقائيًا، ويعالج غير المتزامن التزامن العالي
- حاوية Docker: البناء متعدد المراحل يصغر الصورة، والمستخدم غير الجذر يقوي الأمان، و HEALTHCHECK يراقب الصحة
- تنسيق docker-compose: نشر جاهز للإنتاج مع API + ذاكرة تخزين مؤقت Redis + موازنة تحميل Nginx
- استراتيجية التخزين المؤقت: تخزن Redis نتائج التنبؤ الساخنة مؤقتًا بـ TTL 5 دقائق، محققة معدل إصابة 30-50%
- هدف زمن انتقال API: <50 مللي ثانية (بما في ذلك استدلال النموذج)، مع التنبؤات الدفعية التي تخفض مزيدًا من زمن الانتقال المُطفأ
📝 تمارين
- أساسي (الصعوبة ⭐): درب نموذج sklearn، واحفظه بـ joblib، ثم حمّله في نص Python منفصل وقم بتنبؤ. تلميح: joblib.dump/load.
- متوسط (الصعوبة ⭐⭐): أنشئ نقطة نهاية /predict باستخدام FastAPI، بما في ذلك التحقق من إدخال Pydantic وفحص /health، ثم شغّلها باستخدام uvicorn واختبرها بـ curl. تلميح: راجع كود API الكامل في القسم 4.
- تحدٍّ (الصعوبة ⭐⭐⭐): اكتب Dockerfile (بناء متعدد المراحل) + docker-compose.yml (API + Redis + Nginx)، وابنِ الصورة وشغّل مكدس الخدمة الكامل، ثم تحقق من موازنة التحميل والتخزين المؤقت. تلميح: راجع ملفات التكوين في الأقسام 5-6.