Machine Learning: المراقبة في الإنتاج وانحراف النموذج

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

نشر نموذج مثل إقلاع طائرة - التحدي الحقيقي هو مراقبة الرحلة والتعامل مع أي شيء يأتي في الطريق.

1. ما ستتعلمه


2. قصة حقيقية من مهندس عمليات

(1) الألم: بعد Double-11، خرج كل تنبؤ عن المسار

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

(2) حل مراقبة الانحراف

راقب باستمرار توزيع بيانات الإدخال (PSI) ودقة التنبؤ. في لحظة اكتشاف الانحراف، شغل إعادة التدريب التلقائي.

PYTHON
def check_drift(reference_data, current_data, threshold=0.2):
    psi = calculate_psi(reference_data, current_data)
    if psi > threshold:
        alert("Data drift detected! PSI={:.3f}".format(psi))
        trigger_retraining()

(3) النتيجة: اكتشاف الانحراف في 3 أيام، إصلاحه في أسبوع

بعد أن نشر Bob مراقبة الانحراف، تم اكتشاف تغيير السلوك بعد Double-11 في غضون 3 أيام (PSI=0.45). في غضون أسبوع، اكتملت إعادة التدريب + التحقق A/B + الطرح التدريجي، مستردًا ما يقرب من 300 ألف دولار من خسائر التنبؤ.


3. نظام مراقبة النموذج

(1) بنية مراقبة من ثلاث طبقات

100%
graph TB
    L1[الطبقة 1: البنية التحتية<br/>زمن الانتقال / الإنتاجية / الجهوزية] --> L2[الطبقة 2: مقاييس ML<br/>توزيع التنبؤ / إحصائيات الميزات]
    L2 --> L3[الطبقة 3: المقاييس التجارية<br/>MAPE الإيرادات / معدل التحويل / معدل التخلي]
    L3 --> ALERT{هل تم اكتشاف شذوذ؟}
    ALERT -->|نعم| ACTION[تنبيه → تحقيق → إعادة تدريب]
    ALERT -->|لا| CONTINUE[متابعة المراقبة]

▶ مثال: بيانات لوحة مراقبة أساسية

PYTHON
import numpy as np
from datetime import datetime, timedelta

class ModelMonitor:
    def __init__(self):
        self.metrics_history = []

    def log_prediction(self, prediction, features, latency_ms):
        self.metrics_history.append({
            "timestamp": datetime.now(),
            "prediction": prediction,
            "features_mean": np.mean(features),
            "latency_ms": latency_ms,
        })

    def check_health(self, last_n=1000):
        if len(self.metrics_history) < last_n:
            return "Insufficient data"

        recent = self.metrics_history[-last_n:]
        predictions = [m["prediction"] for m in recent]
        latencies = [m["latency_ms"] for m in recent]

        report = {
            "prediction_mean": np.mean(predictions),
            "prediction_std": np.std(predictions),
            "p95_latency_ms": np.percentile(latencies, 95),
            "sample_count": len(recent),
        }
        return report

monitor = ModelMonitor()
for _ in range(100):
    monitor.log_prediction(prediction=np.random.normal(200, 50),
                           features=np.random.randn(5),
                           latency_ms=np.random.uniform(10, 30))
print(monitor.check_health())

Output:

TEXT 📖 للعرض فقط
# Functions defined successfully
طبقة المراقبة المقياس عتبة التنبيه الأداة
البنية التحتية p95 لزمن انتقال API > 100 مللي ثانية Prometheus
البنية التحتية توفر الخدمة < 99.9% Grafana
مقاييس ML تحول توزيع التنبؤ PSI > 0.2 مخصص
مقاييس ML معدل الميزات المفقودة > 5% مخصص
المقاييس التجارية MAPE التنبؤ > 15% التقارير التجارية
المقاييس التجارية انحراف الإيرادات > 10% التقارير التجارية

4. اكتشاف انحراف البيانات

(1) PSI (مؤشر استقرار السكان)

▶ مثال: حساب PSI

PYTHON
import numpy as np

def calculate_psi(reference, current, n_bins=10):
    """حساب مؤشر استقرار السكان.

    PSI < 0.1: لا تغيير ذا دلالة
    PSI 0.1-0.2: تغيير متوسط، تحقق
    PSI > 0.2: تغيير كبير، إجراء مطلوب
    """
    breakpoints = np.percentile(reference, np.linspace(0, 100, n_bins + 1))
    breakpoints[0] = -np.inf
    breakpoints[-1] = np.inf

    ref_counts = np.histogram(reference, bins=breakpoints)[0]
    cur_counts = np.histogram(current, bins=breakpoints)[0]

    ref_pct = ref_counts / len(reference)
    cur_pct = cur_counts / len(current)

    # تجنب log(0)
    ref_pct = np.clip(ref_pct, 1e-6, None)
    cur_pct = np.clip(cur_pct, 1e-6, None)

    psi = np.sum((cur_pct - ref_pct) * np.log(cur_pct / ref_pct))
    return psi

# محاكاة: بيانات عادية مقابل منحرفة
rng = np.random.default_rng(42)
reference = rng.normal(100, 20, 10000)

# بدون انحراف
current_no_drift = rng.normal(100, 20, 5000)
psi_no_drift = calculate_psi(reference, current_no_drift)

# انحراف متوسط (انحراف المتوسط بمقدار 10)
current_moderate = rng.normal(110, 20, 5000)
psi_moderate = calculate_psi(reference, current_moderate)

# انحراف شديد (انحراف المتوسط بمقدار 25، تأثير Double-11)
current_severe = rng.normal(125, 30, 5000)
psi_severe = calculate_psi(reference, current_severe)

print(f"No drift:    PSI = {psi_no_drift:.3f} (OK)")
print(f"Moderate:    PSI = {psi_moderate:.3f} (Investigate)")
print(f"Severe:      PSI = {psi_severe:.3f} (Action Required!)")

Output:

TEXT 📖 للعرض فقط
# Functions defined successfully

(2) اختبار KS وتباعد JS

▶ مثال: مقارنة مقاييس الانحراف المتعددة

PYTHON
from scipy import stats
import numpy as np

def calculate_js_divergence(reference, current, n_bins=50):
    """تباعد Jensen-Shannon (متماثل، محدود [0, 1])."""
    bins = np.linspace(min(reference.min(), current.min()),
                       max(reference.max(), current.max()), n_bins + 1)
    p = np.histogram(reference, bins=bins, density=True)[0]
    q = np.histogram(current, bins=bins, density=True)[0]
    m = (p + q) / 2
    js = 0.5 * stats.entropy(p, m) + 0.5 * stats.entropy(q, m)
    return js

rng = np.random.default_rng(42)
ref = rng.normal(100, 20, 10000)
cur_drifted = rng.normal(115, 25, 5000)

# اختبار KS
ks_stat, ks_pvalue = stats.ks_2samp(ref, cur_drifted)

# PSI
psi = calculate_psi(ref, cur_drifted)

# تباعد JS
js = calculate_js_divergence(ref, cur_drifted)

print(f"KS statistic: {ks_stat:.4f}, p-value: {ks_pvalue:.6f}")
print(f"PSI:          {psi:.4f}")
print(f"JS Divergence: {js:.4f}")

Output:

TEXT 📖 للعرض فقط
# Functions defined successfully
المقياس النطاق عتبة عدم الانحراف عتبة الانحراف الخصائص
PSI [0, ∞) < 0.1 > 0.2 معيار الصناعة
إحصائية KS [0, 1] < 0.05 > 0.1 اختبار غير معياري
تباعد JS [0, 1] < 0.05 > 0.1 متماثل، قابل للتفسير
تباعد KL [0, ∞) < 0.1 > 0.2 غير متماثل

5. انحراف المفهوم وإعادة التدريب التلقائي

(1) انحراف البيانات مقابل انحراف المفهوم

النوع التعريف طريقة الكشف مثال
انحراف البيانات P(X) يتغير - توزيع الإدخال يتحول PSI/KS على الميزات إنفاق المستخدم يتضاعف خلال Double-11
انحراف المفهوم P(Y|X) يتغير - علاقة المدخلات-المخرجات تتحول ارتفاع خطأ التنبؤ عائد الإعلان ينخفض خلال الترويج
انحراف التسمية P(Y) يتغير - توزيع المخرجات يتحول مراقبة توزيع التنبؤ معدل التخلي يقفز من 8% إلى 15%

▶ مثال: اكتشاف انحراف Double-11 لـ Bob

PYTHON
import numpy as np

rng = np.random.default_rng(42)

# الفترة العادية: علاقة ad_spend → revenue
n_normal = 10000
ad_spend_normal = rng.uniform(10, 100, n_normal)
revenue_normal = 50 + 0.8 * ad_spend_normal + rng.normal(0, 10, n_normal)

# فترة Double-11: السلوك تغير تمامًا
n_promo = 5000
ad_spend_promo = rng.uniform(50, 200, n_promo)  # إنفاق إعلاني أعلى
revenue_promo = 200 + 1.5 * ad_spend_promo + rng.normal(0, 30, n_promo)  # معاملات مختلفة!

# اكتشاف الانحراف في توزيع ad_spend
psi = calculate_psi(ad_spend_normal, ad_spend_promo)
ks_stat, ks_p = stats.ks_2samp(ad_spend_normal, ad_spend_promo)

print("=== Data Drift Detection ===")
print(f"Ad spend PSI: {psi:.3f} {'⚠️ DRIFT' if psi > 0.2 else 'OK'}")
print(f"Ad spend KS:  {ks_stat:.3f} (p={ks_p:.6f}) {'⚠️ DRIFT' if ks_p < 0.05 else 'OK'}")

# اكتشاف انحراف المفهوم: نفس X، علاقة Y|X مختلفة
# استخدم نموذجًا مدربًا على البيانات العادية للتنبؤ بالبيانات الترويجية
from sklearn.linear_model import LinearRegression
model_normal = LinearRegression()
model_normal.fit(ad_spend_normal.reshape(-1, 1), revenue_normal)

pred_promo = model_normal.predict(ad_spend_promo.reshape(-1, 1))
mape_normal_on_normal = np.mean(np.abs((revenue_normal - model_normal.predict(ad_spend_normal.reshape(-1, 1))) / revenue_normal)) * 100
mape_normal_on_promo = np.mean(np.abs((revenue_promo - pred_promo) / revenue_promo)) * 100

print(f"\n=== Concept Drift Detection ===")
print(f"MAPE (normal→normal): {mape_normal_on_normal:.1f}%")
print(f"MAPE (normal→promo):  {mape_normal_on_promo:.1f}%")
print(f"Performance degradation: {mape_normal_on_promo - mape_normal_on_normal:.1f}% ⚠️ CONCEPT DRIFT")

Output:

TEXT 📖 للعرض فقط
=== Data Drift Detection ===

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

▶ مثال: إطار اكتشاف الانحراف + إعادة التدريب التلقائي

PYTHON
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_percentage_error

class AutoRetrainPipeline:
    def __init__(self, drift_threshold_psi=0.2, mape_threshold=0.15):
        self.drift_threshold_psi = drift_threshold_psi
        self.mape_threshold = mape_threshold
        self.model = None
        self.reference_data = None

    def train(self, X, y):
        self.model = LinearRegression()
        self.model.fit(X, y)
        self.reference_data = X.copy()

    def check_drift(self, X_current):
        drifts = []
        for i in range(X_current.shape[1]):
            psi = calculate_psi(self.reference_data[:, i], X_current[:, i])
            drifts.append({"feature": f"feat_{i}", "psi": psi, "drift": psi > self.drift_threshold_psi})
        return drifts

    def check_performance(self, X, y):
        y_pred = self.model.predict(X)
        mape = mean_absolute_percentage_error(y, y_pred)
        return mape < self.mape_threshold, mape

    def auto_retrain_if_needed(self, X_current, y_current):
        # الخطوة 1: تحقق من الانحراف
        drifts = self.check_drift(X_current)
        any_drift = any(d["drift"] for d in drifts)

        # الخطوة 2: تحقق من الأداء
        performance_ok, mape = self.check_performance(X_current, y_current)

        # الخطوة 3: القرار
        if not any_drift and performance_ok:
            return "OK - No retraining needed"

        if any_drift and not performance_ok:
            # أعد التدريب مع البيانات الحديثة
            self.model.fit(X_current, y_current)
            self.reference_data = X_current.copy()
            new_mape = mean_absolute_percentage_error(y_current, self.model.predict(X_current))
            return f"RETRAINED - PSI drift detected, MAPE {mape:.1%} → {new_mape:.1%}"

        if any_drift:
            return f"DRIFT WARNING - PSI drift but performance still OK (MAPE {mape:.1%})"

        return f"PERFORMANCE WARNING - No drift but MAPE degraded to {mape:.1%}"

# اختبار
pipeline = AutoRetrainPipeline()
rng = np.random.default_rng(42)
X_train = rng.uniform(10, 100, (5000, 3))
y_train = 50 + 0.8 * X_train[:, 0] + rng.normal(0, 5, 5000)
pipeline.train(X_train, y_train)

# محاكاة بيانات Double-11
X_promo = rng.uniform(50, 200, (2000, 3))
y_promo = 200 + 1.5 * X_promo[:, 0] + rng.normal(0, 10, 2000)

result = pipeline.auto_retrain_if_needed(X_promo, y_promo)
print(result)

Output:

TEXT 📖 للعرض فقط
# Functions defined successfully

❓ أسئلة شائعة

س هل يعني PSI > 0.2 دائمًا أننا بحاجة إلى إعادة التدريب؟
ج ليس بالضرورة. يكتشف PSI فقط تغيير التوزيع، وليس تدهور الأداء. قد يتحول التوزيع بينما يظل النموذج دقيقًا (مثلًا مستخدمون أكثر ولكن نفس الأنماط). من الأفضل تشغيل إعادة التدريب على شرط مزدوج: PSI + MAPE.
س كم مرة يجب أن نتحقق من الانحراف؟
ج ذلك يعتمد على إيقاع عملك - تحقق كل ساعة للخدمات في الوقت الفعلي، ويوميًا للمهام الدفعية، وأسبوعيًا للتنبؤات الشهرية. تحقق دائمًا فورًا بعد الأحداث الكبرى (مثل Double-11).
س ما البيانات التي يجب أن نستخدمها لإعادة التدريب؟
ج يوصى بنافذة منزلقة (أحدث 3-6 أشهر من البيانات)، أو تدريب موزون (إعطاء البيانات الحديثة وزنًا أعلى). تجنب استخدام جميع البيانات التاريخية - قد تعكس البيانات الأقدم مفهومًا قديمًا.
س أيهما أكثر خطورة، انحراف المفهوم أم انحراف البيانات؟
ج انحراف المفهوم أكثر خطورة - مع انحراف البيانات يتغير فقط توزيع المدخلات، لذلك قد يظل النموذج دقيقًا؛ مع انحراف المفهوم تتغير علاقة المدخلات-المخرجات، لذلك يكون النموذج مضمونًا أن يكون خاطئًا. ومع ذلك، غالبًا ما يكون انحراف البيانات علامة تحذير مبكر لانحراف المفهوم.
س هل إعادة التدريب التلقائي محفوفة بالمخاطر؟
ج نعم - 1) قد تحتوي البيانات الجديدة على مشكلات جودة التسمية؛ 2) يمكن لإعادة التدريب التلقائي أن تقدم أخطاء جديدة؛ 3) يجب أن يمر التحقق A/B قبل البث المباشر. سير العمل الموصى به: إعادة التدريب التلقائي + المراجعة البشرية + التحقق A/B، في ثلاث خطوات.
س كيف نفصل التقلب الموسمي عن الانحراف الحقيقي؟
ج استخدم المقارنة من سنة إلى أخرى (الربع الرابع هذا العام مقابل الربع الرابع العام الماضي) بدلاً من فترة إلى فترة (الربع الرابع مقابل الربع الثالث). الموسمية دورية (تتكرر كل عام)؛ الانحراف هيكلي (تحول مستمر لا يعود).

📖 ملخص

📝 تمارين

  1. أساسي (الصعوبة ⭐): استخدم PSI لاكتشاف تغيير التوزيع بين مجموعتي بيانات، وقارن قيم PSI لحالة عدم الانحراف مقابل انحراف المتوسط بمقدار 10. تلميح: راجع دالة حساب PSI في القسم 4.
  2. متوسط (الصعوبة ⭐⭐): نفّذ مراقبة من ثلاث طبقات - لبيانات التنبؤ المحاكاة، اكتشف: 1) انحراف PSI؛ 2) تدهور MAPE؛ 3) شذوذ زمن الانتقال (p95 > 100 مللي ثانية). تلميح: ثلاث دوال تحقق مستقلة + تقرير مدمج.
  3. تحدٍّ (الصعوبة ⭐⭐⭐): نفّذ AutoRetrainPipeline كامل - ولّد بيانات فترة عادية + فترة ترويج، ودرب نموذجًا أوليًا، واكتشف الانحراف وأعد التدريب تلقائيًا، وقارن MAPE قبل وبعد إعادة التدريب، وأخرج تقرير مراقبة كامل. تلميح: راجع إطار إعادة التدريب التلقائي في القسم 5.

← الدرس السابق: اختبار A/B | الدرس التالي: تصميم المشروع →

Web-Tutorial.com

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

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

100%