Machine Learning: نشر المشروع

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

النشر هو نهاية وبداية - يعني البث المباشر أن العمليات تبدأ، والموثوقية المستمرة هي ما يبدو عليه التسليم الحقيقي.

1. ما ستتعلمه


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

(1) نقطة الألم: تم تدريب النموذج، لكن خطة النشر كانت غير مكتملة

درب Bob نموذج LightGBM بـ MAPE 8%، لكن خطة النشر الخاصة به لم تتكون من أكثر من FastAPI + Docker - لا مراقبة، لا تخزين مؤقت، لا تحديد للمعدل، لا آلية إعادة تدريب. في اليوم الأول، تسبب ارتفاع حركة المرور في انتهاء مهلة API. في اليوم الثاني، تسبب تغيير تنسيق الميزة في أن يكون كل تنبؤ خاطئًا. النشر بدون عمليات مثل السيارة بدون فرامل - إنها مجرد مسألة وقت قبل أن يحدث خطأ ما.

(2) الحل: نشر جاهز للإنتاج كامل

نشر جاهز للإنتاج = خدمة (API) + تنسيق (Docker) + مراقبة (Grafana) + تنبيه (Prometheus) + إعادة تدريب (Airflow).

YAML
# مكدس النشر الجاهز للإنتاج
services:
  api: FastAPI + Uvicorn (خدمة النموذج)
  redis: طبقة التخزين المؤقت (التنبؤات الساخنة)
  nginx: موازن التحميل + تحديد المعدل
  prometheus: جمع المقاييس
  grafana: لوحة المراقبة
  airflow: جدولة إعادة التدريب

(3) النتيجة: ثلاثة أشهر مباشرة بدون حوادث، MAPE مستقر عند 8%

بعد إكمال النشر الكامل، ظل Bob يعمل لمدة ثلاثة أشهر بدون حوادث. ظل MAPE للنموذج مستقرًا عند 8%، وتم اكتشاف حدث انحراف Double-11 تلقائيًا وإصلاحه من خلال إعادة التدريب في غضون ثلاثة أيام.


3. خدمة استدلال FastAPI

(1) تنفيذ API جاهز للإنتاج

▶ مثال: خدمة FastAPI الكاملة

PYTHON
# File: app/main.py
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from typing import Optional
import joblib
import numpy as np
import time
import logging

logger = logging.getLogger("salespredict")
app = FastAPI(title="SalesPredict API", version="1.0.0")

# النموذج العام (يُحمّل عند البدء)
model = None

@app.on_event("startup")
async def load_model():
    global model
    model = joblib.load("model/salespredict_lgbm.joblib")
    logger.info("Model loaded successfully")

class PredictionInput(BaseModel):
    ad_spend_k_usd: float = Field(..., ge=0, le=1000)
    traffic_k: float = Field(..., ge=0, le=10000)
    is_promotion: int = Field(0, ge=0, le=1)
    is_weekend: int = Field(0, ge=0, le=1)
    category_clothing: int = Field(0, ge=0, le=1)
    category_electronics: int = Field(0, ge=0, le=1)
    category_food: int = Field(0, ge=0, le=1)
    category_home: int = Field(0, ge=0, le=1)
    region_EU: int = Field(0, ge=0, le=1)
    region_US: int = Field(0, ge=0, le=1)

    model_config = {"json_schema_extra": {
        "example": {"ad_spend_k_usd": 50, "traffic_k": 300,
                     "is_promotion": 1, "is_weekend": 0,
                     "category_electronics": 1, "category_clothing": 0,
                     "category_food": 0, "category_home": 0,
                     "region_US": 1, "region_EU": 0}
    }}

class PredictionOutput(BaseModel):
    predicted_revenue_k_usd: float
    confidence_low: Optional[float] = None
    confidence_high: Optional[float] = None
    latency_ms: float

@app.get("/health")
def health():
    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_usd, input_data.traffic_k,
                               input_data.is_promotion, input_data.is_weekend,
                               input_data.category_clothing, input_data.category_electronics,
                               input_data.category_food, input_data.category_home,
                               input_data.region_EU, input_data.region_US]])
        prediction = float(model.predict(features)[0])
        latency = (time.time() - start) * 1000

        # فترة ثقة بسيطة (±20%)
        return PredictionOutput(
            predicted_revenue_k_usd=round(prediction, 2),
            confidence_low=round(prediction * 0.8, 2),
            confidence_high=round(prediction * 1.2, 2),
            latency_ms=round(latency, 2),
        )
    except Exception as e:
        logger.error(f"Prediction error: {e}")
        raise HTTPException(status_code=500, detail=str(e))

@app.post("/predict_batch")
def predict_batch(inputs: list[PredictionInput], max_batch: int = 100):
    if len(inputs) > max_batch:
        raise HTTPException(status_code=400, detail=f"Batch size exceeds {max_batch}")
    start = time.time()
    features = np.array([[d.ad_spend_k_usd, d.traffic_k, d.is_promotion, d.is_weekend,
                           d.category_clothing, d.category_electronics, d.category_food,
                           d.category_home, d.region_EU, d.region_US] for d in inputs])
    predictions = model.predict(features)
    latency = (time.time() - start) * 1000
    return {
        "predictions": [round(float(p), 2) for p in predictions],
        "count": len(predictions),
        "latency_ms": round(latency, 2),
    }

@app.middleware("http")
async def log_requests(request: Request, call_next):
    start = time.time()
    response = await call_next(request)
    latency = (time.time() - start) * 1000
    logger.info(f"{request.method} {request.url.path} - {response.status_code} - {latency:.1f}ms")
    return response

Output:

TEXT 📖 للعرض فقط
INFO:     Application startup complete.
INFO:     Model loaded successfully
INFO:     Uvicorn running on http://0.0.0.0:8000

(2) مقاييس أداء API

نقطة النهاية زمن انتقال فردي زمن انتقال دفعي (100) QPS
/predict < 10 مللي ثانية 100+
/predict_batch < 50 مللي ثانية 50+
/health < 1 مللي ثانية 1000+

4. نشر Docker

(1) Dockerfile متعدد المراحل

▶ مثال: Dockerfile جاهز للإنتاج

DOCKERFILE
# Stage 1: Build
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
FROM python:3.11-slim
WORKDIR /app

COPY --from=builder /install /usr/local
COPY app/ ./app/
COPY model/ ./model/

RUN useradd -m -r appuser && chown -R appuser:appuser /app
USER appuser

EXPOSE 8000

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

Output:

TEXT 📖 للعرض فقط
Successfully built 3a7f2b1c9d4e
Successfully tagged salespredict-api:latest

(2) تنسيق docker-compose

▶ مثال: مكدس الخدمة الكامل

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

services:
  api:
    build: .
    environment:
      - REDIS_URL=redis://redis:6379/0
      - MLFLOW_TRACKING_URI=http://mlflow:5000
    depends_on:
      - redis
    deploy:
      replicas: 2
      resources:
        limits:
          memory: 2G
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
      interval: 30s
      timeout: 5s

  redis:
    image: redis:7-alpine
    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - redis_data:/data
    restart: unless-stopped

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

  prometheus:
    image: prom/prometheus:latest
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana_data:/var/lib/grafana
    depends_on:
      - prometheus
    restart: unless-stopped

volumes:
  redis_data:
  prometheus_data:
  grafana_data:
TEXT 📖 للعرض فقط
# nginx.conf
upstream api_backend {
    least_conn;
    server api:8000;
}

server {
    listen 80;
    location / {
        proxy_pass http://api_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
    location /health {
        proxy_pass http://api_backend/health;
    }
}

5. لوحات المراقبة

(1) جمع مقاييس Prometheus

▶ مثال: كشف مقاييس API

PYTHON
# Add to app/main.py
from prometheus_client import Counter, Histogram, generate_latest
from fastapi import Response

PREDICTIONS_COUNT = Counter("predictions_total", "Total predictions made")
PREDICTION_LATENCY = Histogram("prediction_latency_seconds", "Prediction latency",
                                buckets=[0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0])

@app.get("/metrics")
def metrics():
    return Response(content=generate_latest(), media_type="text/plain")

# Instrument the predict endpoint
@app.post("/predict", response_model=PredictionOutput)
def predict(input_data: PredictionInput):
    start = time.time()
    PREDICTIONS_COUNT.inc()
    # ... (prediction logic) ...
    PREDICTION_LATENCY.observe(time.time() - start)
    # ... (return result) ...

Output:

TEXT 📖 للعرض فقط
INFO:     Metrics endpoint registered at /metrics
INFO:     Prediction counter initialized

(2) تكوين لوحة Grafana

اللوحة المقياس عتبة التنبيه
QPS التنبؤ rate(predictions_total[5m]) < 10 (منخفض بشكل غير طبيعي)
زمن الانتقال p95 histogram_quantile(0.95, prediction_latency_seconds) > 100 مللي ثانية
معدل الخطأ rate(http_requests_total{status=~"5xx"}[5m]) > 1%
متوسط التنبؤ avg(predicted_revenue_k_usd) انحراف 20% عن خط الأساس

▶ مثال: نص مراقبة الانحراف

PYTHON
# File: monitoring/drift_monitor.py
import numpy as np
import requests
import json
from datetime import datetime

class DriftMonitor:
    def __init__(self, reference_stats_path, api_url="http://localhost:8000"):
        self.reference = np.load(reference_stats_path, allow_pickle=True).item()
        self.api_url = api_url

    def calculate_psi(self, reference, current, n_bins=10):
        breakpoints = np.percentile(reference, np.linspace(0, 100, n_bins + 1))
        breakpoints[0], breakpoints[-1] = -np.inf, np.inf
        ref_counts = np.histogram(reference, bins=breakpoints)[0]
        cur_counts = np.histogram(current, bins=breakpoints)[0]
        ref_pct = np.clip(ref_counts / len(reference), 1e-6, None)
        cur_pct = np.clip(cur_counts / len(current), 1e-6, None)
        return np.sum((cur_pct - ref_pct) * np.log(cur_pct / ref_pct))

    def check_weekly_drift(self, current_features):
        """قم بإجراء فحص انحراف أسبوعي على جميع الميزات."""
        results = {}
        for feature_name, current_data in current_features.items():
            if feature_name in self.reference:
                psi = self.calculate_psi(self.reference[feature_name], current_data)
                results[feature_name] = {
                    "psi": round(psi, 3),
                    "status": "OK" if psi < 0.1 else ("WARNING" if psi < 0.2 else "DRIFT"),
                }

        # سجل النتائج
        drift_detected = any(r["status"] == "DRIFT" for r in results.values())
        if drift_detected:
            self._send_alert(results)

        return results, drift_detected

    def _send_alert(self, results):
        alert_msg = f"DRIFT ALERT at {datetime.now()}\n"
        for feat, info in results.items():
            if info["status"] != "OK":
                alert_msg += f"  {feat}: PSI={info['psi']} ({info['status']})\n"
        print(alert_msg)
        # In production: send to Slack/PagerDuty

# Weekly monitoring job
monitor = DriftMonitor("model/reference_stats.npy")
# features = load_current_week_features()
# results, drift = monitor.check_weekly_drift(features)

Output:

TEXT 📖 للعرض فقط
DriftMonitor initialized with reference stats
Weekly check scheduled: PSI threshold=0.2

6. إعادة التدريب التلقائي ومراجعة المشروع

(1) خط أنابيب إعادة التدريب التلقائي

▶ مثال: مفهوم Airflow DAG

PYTHON
# File: dags/retrain_pipeline.py (Airflow DAG concept)
# from airflow import DAG
# from airflow.operators.python import PythonOperator

def retrain_pipeline():
    """خط أنابيب إعادة التدريب التلقائي الذي يتم تشغيله بواسطة اكتشاف الانحراف."""
    # الخطوة 1: تحقق من الانحراف
    # drift_detected = check_weekly_drift()

    # الخطوة 2: جلب البيانات الحديثة
    # data = fetch_recent_data(months=3)

    # الخطوة 3: أعد تدريب النموذج
    # new_model, metrics = train_lightgbm(data)

    # الخطوة 4: قارن مع الإنتاج
    # if metrics["mape"] < production_mape:
    #     register_model(new_model, stage="Staging")

    # الخطوة 5: اختبار A/B (أسبوعين)
    # run_ab_test(new_model, duration_weeks=2)

    # الخطوة 6: الترقية إذا أظهر A/B تحسنًا
    # if ab_test_significant:
    #     promote_to_production(new_model)

    # الخطوة 7: تحديث إحصائيات المرجع
    # update_reference_stats(new_model)
    pass

# تعريف DAG
# with DAG("salespredict_retrain", schedule_interval="0 6 * * 1") as dag:
#     check_drift = PythonOperator(task_id="check_drift", python_callable=check_drift)
#     retrain = PythonOperator(task_id="retrain", python_callable=retrain_model)
#     ab_test = PythonOperator(task_id="ab_test", python_callable=run_ab_test)
#     promote = PythonOperator(task_id="promote", python_callable=promote_model)
#     check_drift >> retrain >> ab_test >> promote

Output:

TEXT 📖 للعرض فقط
DAG 'salespredict_retrain' registered
Schedule: every Monday 06:00 UTC
Tasks: check_drift >> retrain >> ab_test >> promote

(2) مراجعة المشروع

100%
graph TB
    START[الأسبوع 1: إطلاق المشروع] --> DATA[الأسبوع 2-3: خط أنابيب البيانات]
    DATA --> BASELINE[الأسبوع 3: خط الأساس LR<br/>MAPE 15%]
    BASELINE --> XGB[الأسبوع 4-5: XGBoost<br/>MAPE 9%]
    XGB --> LGBM[الأسبوع 5-6: LightGBM + Optuna<br/>MAPE 8%]
    LGBM --> DEPLOY[الأسبوع 7: FastAPI + Docker]
    DEPLOY --> MONITOR[الأسبوع 8: المراقبة + الانحراف]
    MONITOR --> LIVE[مباشر في الإنتاج<br/>MAPE 8% مستقر]
البُعد القيمة البدئية القيمة النهائية التحسن
MAPE التنبؤ 25% (Excel) 8% (LightGBM) 68%↓
تكلفة المخزون 500 ألف دولار/سنة 100 ألف دولار/سنة 400 ألف موفرة
زمن انتقال التنبؤ يومين (يدوي) 10 مللي ثانية (API) 170 مليون×↓
إنتاجية التجارب 3/أسبوع 80+/يوم 180×↑

▶ مثال: مراجعة شاملة

PYTHON
project_retrospective = {
    "what_went_well": [
        "منع تتبع تجارب MLflow الالتباس على أكثر من 50 تشغيل",
        "اكتشف TimeSeriesSplit تسرب البيانات المستقبلية قبل الإنتاج",
        "وجد Optuna معاملات أفضل من البحث اليدوي في 1/10 من الوقت",
        "مكن نشر Docker من تحديثات النموذج بدون توقف",
    ],
    "what_could_improve": [
        "كان يجب بناء خط أنابيب البيانات قبل تدريب النموذج",
        "كان من شأن مخزن الميزات تقليل الازدواجية بين التدريب والخدمة",
        "كان يجب تشغيل اختبار A/B لفترة أطول (3 أسابيع بدلاً من 2)",
        "كان يجب إعداد لوحة المراقبة من اليوم الأول",
    ],
    "key_learnings": [
        "جودة البيانات > تعقيد النموذج (بيانات نظيفة + نموذج بسيط تتفوق على بيانات متسخة + نموذج معقد)",
        "التصميم أولًا، الكود ثانيًا (أسبوع تصميم وفّر 4 أسابيع من إعادة العمل)",
        "انشر مبكرًا، تكرار بسرعة (تغذية راجعة من الإنتاج > تجارب المختبر)",
        "راقب كل شيء (اكتشف اكتشاف الانحراف مشكلة Double-11 في 3 أيام)",
    ],
    "next_steps": [
        "أضف نموذج LSTM لالتقاط الأنماط الزمنية",
        "نفّذ خدمة الميزات في الوقت الفعلي مع Feast",
        "ابنِ خط أنابيب إعادة التدريب التلقائي مع Airflow",
        "وسّع إلى 3 أسواق إضافية (اليابان، الهند، البرازيل)",
    ],
}

for category, items in project_retrospective.items():
    print(f"\n{category.replace('_', ' ').title()}:")
    for item in items:
        print(f"  - {item}")

Output:

TEXT 📖 للعرض فقط
What Went Well:
  - منع تتبع تجارب MLflow الالتباس على أكثر من 50 تشغيل
  - اكتشف TimeSeriesSplit تسرب البيانات المستقبلية قبل الإنتاج
What Could Improve:
  - كان يجب بناء خط أنابيب البيانات قبل تدريب النموذج
Key Learnings:
  - جودة البيانات > تعقيد النموذج
Next Steps:
  - أضف نموذج LSTM لالتقاط الأنماط الزمنية

❓ أسئلة شائعة

س كم عدد الموارد التي تحتاجها خدمة API؟
ج استدلال LightGBM مكثف لوحدة المعالجة المركزية - يمكن لنواتين و 4 جيجابايت RAM التعامل مع 100+ QPS. بالنسبة لـ PyTorch MLP، استهدف 4 نوى، 8 جيجابايت+، ووحدة معالجة رسومات إذا كان النموذج كبيرًا. 256 ميجابايت من ذاكرة التخزين المؤقت Redis أكثر من كافية.
س كيف تحدث النموذج دون توقف؟
ج نهجان — 1) النشر الأزرق-الأخضر (التبديل بين الإصدارين القديم والجديد عن طريق تحويل حركة المرور)؛ 2) التحديثات المتدرجة (تحديث تدريجي لـ docker-compose، استبدال النسخ واحدة تلو الأخرى). اقترن أي منهما بإدارة إصدارات MLflow.
س ما المقاييس التي يجب أن تتبعها لوحة المراقبة؟
ج ثلاث طبقات — البنية التحتية (زمن الانتقال، التوفر، الذاكرة)، مقاييس ML (توزيع التنبؤ، إحصائيات الميزات، إصدار النموذج)، والمقاييس التجارية (MAPE، انحراف الإيرادات، معدل التحويل). يجب أن تؤدي الحالات الشاذة في أي طبقة إلى تنبيه.
س كم مرة يجب أن تعمل إعادة التدريب التلقائي؟
ج افتراضيًا، تحقق من الانحراف أسبوعيًا وأعد التدريب شهريًا (مع تحديثات دورية حتى عندما لا يكون هناك انحراف). شغل فحصًا فوريًا بعد الأحداث الرئيسية (العروض الترويجية، إطلاق المنتجات الجديدة). يؤدي الانحراف إلى إعادة تدريب فورية.
س ماذا تفعل إذا تدهور النموذج بعد النشر؟
ج استجابة من أربع خطوات — 1) تحقق من انحراف البيانات (PSI)؛ 2) تحقق من انحراف المفهوم (اتجاه MAPE)؛ 3) أعد تدريب وتحقق؛ 4) طرح تدريجي بعد اختبار A/B. سجل كل شيء في MLflow طوال الوقت.
س لقد أكملت جميع الدروس الـ 25 - ماذا أتعلم بعد ذلك؟
ج ثلاث اتجاهات — 1) التعلم العميق (Transformers، NLP، الرؤية الحاسوبية)؛ 2) MLOps المتقدم (Kubeflow، مخازن الميزات، إصدارات البيانات)؛ 3) مزيد من المشاريع العملية (Kaggle، مساهمات مفتوحة المصدر). أسسك صلبة - اختر الاتجاه الذي يثير اهتمامك وتعمق فيه.

📖 ملخص

📝 تمارين

  1. أساسي (الصعوبة ⭐): احفظ نموذجك المدرب كملف joblib، واكتب نقطة نهاية FastAPI /predict بسيطة، ثم شغلها باستخدام uvicorn واختبرها. تلميح: راجع كود API في القسم 3.
  2. متوسط (الصعوبة ⭐⭐): اكتب Dockerfile + docker-compose.yml (API + Redis + Nginx)، وابنِ الصورة، وشغّل مكدس الخدمة الكامل، وتحقق من نقطة نهاية /health وموازنة التحميل. تلميح: راجع تكوين Docker في القسم 4.
  3. تحدٍّ (الصعوبة ⭐⭐⭐): نفّذ نشر إنتاج كامل - FastAPI + مقاييس Prometheus + لوحة Grafana + نص مراقبة الانحراف. شغّلها لمدة أسبوع على بيانات محاكاة، واكتشف انحرافًا محقونًا، وشغل تنبيهًا. تلميح: ادمج جميع الأكواد من الأقسام 3-6.

← الدرس السابق: تطوير المشروع | نهاية البرنامج التعليمي →

Web-Tutorial.com

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

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

100%