Machine Learning: نشر المشروع
آخر تحديث: 2026-08-26
النشر هو نهاية وبداية - يعني البث المباشر أن العمليات تبدأ، والموثوقية المستمرة هي ما يبدو عليه التسليم الحقيقي.
1. ما ستتعلمه
- خدمة استدلال FastAPI: التحقق من الطلبات، التنبؤ الدفعي، استراتيجيات التخزين المؤقت (Redis)، تحديد المعدل والتدهور السلس
- نشر Docker: Dockerfile متعدد المراحل، تنسيق docker-compose، تكوين فحص الصحة
- لوحات المراقبة: Grafana + Prometheus للتتبع في الوقت الفعلي لحجم التنبؤ وزمن الانتقال والدقة والإيرادات
- اكتشاف الانحراف وإعادة التدريب التلقائي: فحوصات PSI أسبوعية تشغل خط أنابيب إعادة تدريب Airflow عند تجاوز العتبات
- مراجعة المشروع: مراجعة شاملة من البداية إلى النهاية لرحلة SalesPredict لـ Bob من الصفر إلى الواحد
2. قصة حقيقية من مهندس DevOps
(1) نقطة الألم: تم تدريب النموذج، لكن خطة النشر كانت غير مكتملة
درب Bob نموذج LightGBM بـ MAPE 8%، لكن خطة النشر الخاصة به لم تتكون من أكثر من FastAPI + Docker - لا مراقبة، لا تخزين مؤقت، لا تحديد للمعدل، لا آلية إعادة تدريب. في اليوم الأول، تسبب ارتفاع حركة المرور في انتهاء مهلة API. في اليوم الثاني، تسبب تغيير تنسيق الميزة في أن يكون كل تنبؤ خاطئًا. النشر بدون عمليات مثل السيارة بدون فرامل - إنها مجرد مسألة وقت قبل أن يحدث خطأ ما.
(2) الحل: نشر جاهز للإنتاج كامل
نشر جاهز للإنتاج = خدمة (API) + تنسيق (Docker) + مراقبة (Grafana) + تنبيه (Prometheus) + إعادة تدريب (Airflow).
# مكدس النشر الجاهز للإنتاج
services:
api: FastAPI + Uvicorn (خدمة النموذج)
redis: طبقة التخزين المؤقت (التنبؤات الساخنة)
nginx: موازن التحميل + تحديد المعدل
prometheus: جمع المقاييس
grafana: لوحة المراقبة
airflow: جدولة إعادة التدريب
(3) النتيجة: ثلاثة أشهر مباشرة بدون حوادث، MAPE مستقر عند 8%
بعد إكمال النشر الكامل، ظل Bob يعمل لمدة ثلاثة أشهر بدون حوادث. ظل MAPE للنموذج مستقرًا عند 8%، وتم اكتشاف حدث انحراف Double-11 تلقائيًا وإصلاحه من خلال إعادة التدريب في غضون ثلاثة أيام.
3. خدمة استدلال FastAPI
(1) تنفيذ API جاهز للإنتاج
▶ مثال: خدمة FastAPI الكاملة
# 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:
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 جاهز للإنتاج
# 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:
Successfully built 3a7f2b1c9d4e
Successfully tagged salespredict-api:latest
(2) تنسيق docker-compose
▶ مثال: مكدس الخدمة الكامل
# 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:
# 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
# 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:
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% عن خط الأساس |
▶ مثال: نص مراقبة الانحراف
# 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:
DriftMonitor initialized with reference stats
Weekly check scheduled: PSI threshold=0.2
6. إعادة التدريب التلقائي ومراجعة المشروع
(1) خط أنابيب إعادة التدريب التلقائي
▶ مثال: مفهوم Airflow DAG
# 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:
DAG 'salespredict_retrain' registered
Schedule: every Monday 06:00 UTC
Tasks: check_drift >> retrain >> ab_test >> promote
(2) مراجعة المشروع
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×↑ |
▶ مثال: مراجعة شاملة
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:
What Went Well:
- منع تتبع تجارب MLflow الالتباس على أكثر من 50 تشغيل
- اكتشف TimeSeriesSplit تسرب البيانات المستقبلية قبل الإنتاج
What Could Improve:
- كان يجب بناء خط أنابيب البيانات قبل تدريب النموذج
Key Learnings:
- جودة البيانات > تعقيد النموذج
Next Steps:
- أضف نموذج LSTM لالتقاط الأنماط الزمنية
❓ أسئلة شائعة
📖 ملخص
- خدمة FastAPI الإنتاجية: التحقق من Pydantic، التنبؤ الدفعي، تسجيل الوسيط، مقاييس Prometheus
- نشر Docker: بناء متعدد المراحل لصور أصغر، تنسيق docker-compose للمكدس الكامل، HEALTHCHECK لفحوصات الصحة
- لوحات المراقبة: جمع Prometheus + تصور Grafana، ثلاث طبقات من المراقبة (البنية التحتية / ML / الأعمال)
- اكتشاف الانحراف: فحوصات PSI أسبوعية، تنبيهات تُثار فوق 0.2، Airflow DAG ينسق خط أنابيب إعادة التدريب
- إعادة التدريب التلقائي: الاكتشاف → ملء البيانات للخلف → التدريب → التحقق A/B → الطرح التدريجي، تلقائي بالكامل من البداية إلى النهاية
- SalesPredict من البداية إلى النهاية: من 25% MAPE في Excel إلى 8% MAPE مع LightGBM، مما يوفر 400 ألف دولار سنويًا في تكاليف المخزون
📝 تمارين
- أساسي (الصعوبة ⭐): احفظ نموذجك المدرب كملف joblib، واكتب نقطة نهاية FastAPI /predict بسيطة، ثم شغلها باستخدام uvicorn واختبرها. تلميح: راجع كود API في القسم 3.
- متوسط (الصعوبة ⭐⭐): اكتب Dockerfile + docker-compose.yml (API + Redis + Nginx)، وابنِ الصورة، وشغّل مكدس الخدمة الكامل، وتحقق من نقطة نهاية /health وموازنة التحميل. تلميح: راجع تكوين Docker في القسم 4.
- تحدٍّ (الصعوبة ⭐⭐⭐): نفّذ نشر إنتاج كامل - FastAPI + مقاييس Prometheus + لوحة Grafana + نص مراقبة الانحراف. شغّلها لمدة أسبوع على بيانات محاكاة، واكتشف انحرافًا محقونًا، وشغل تنبيهًا. تلميح: ادمج جميع الأكواد من الأقسام 3-6.