Machine Learning: المراقبة في الإنتاج وانحراف النموذج
آخر تحديث: 2026-08-26
نشر نموذج مثل إقلاع طائرة - التحدي الحقيقي هو مراقبة الرحلة والتعامل مع أي شيء يأتي في الطريق.
1. ما ستتعلمه
- أنظمة مراقبة النموذج: مراقبة توزيع التنبؤ، مراقبة زمن الانتقال، مراقبة المقاييس التجارية
- انحراف البيانات: PSI / اختبار KS / تباعد Jensen-Shannon، واكتشاف التحولات في توزيعات الميزات
- انحراف المفهوم: التغييرات في علاقة المدخلات-المخرجات، مع الكشف التفاعلي والاستباقي
- خط أنابيب إعادة التدريب التلقائي: تنبيه الانحراف → ملء البيانات للخلف → إعادة التدريب → التحقق A/B → طرح تدريجي
- قصة حرب انحراف Bob: كيف غيّر مهرجان Double-11 للتسوق سلوك المستخدم بشكل جذري، وكيف تكيف SalesPredict بسرعة
2. قصة حقيقية من مهندس عمليات
(1) الألم: بعد Double-11، خرج كل تنبؤ عن المسار
كان نموذج SalesPredict لـ Bob يعمل بسلاسة، مع ثبات MAPE عند 8%. ولكن بعد ترويج Double-11، قفز خطأ التنبؤ إلى 35% - لقد تغير سلوك المستخدم تمامًا (الشراء الاندفاعي، تحولات في تفضيلات الفئة، دورات شراء مضغوطة)، بينما كان النموذج لا يزال يتنبأ بناءً على أنماط ما قبل الترويج. تدهور النموذج بعد النشر صامت - بحلول وقت ملاحظته، كان الضرر قد وقع بالفعل.
(2) حل مراقبة الانحراف
راقب باستمرار توزيع بيانات الإدخال (PSI) ودقة التنبؤ. في لحظة اكتشاف الانحراف، شغل إعادة التدريب التلقائي.
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) بنية مراقبة من ثلاث طبقات
graph TB
L1[الطبقة 1: البنية التحتية<br/>زمن الانتقال / الإنتاجية / الجهوزية] --> L2[الطبقة 2: مقاييس ML<br/>توزيع التنبؤ / إحصائيات الميزات]
L2 --> L3[الطبقة 3: المقاييس التجارية<br/>MAPE الإيرادات / معدل التحويل / معدل التخلي]
L3 --> ALERT{هل تم اكتشاف شذوذ؟}
ALERT -->|نعم| ACTION[تنبيه → تحقيق → إعادة تدريب]
ALERT -->|لا| CONTINUE[متابعة المراقبة]
▶ مثال: بيانات لوحة مراقبة أساسية
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:
# Functions defined successfully
| طبقة المراقبة | المقياس | عتبة التنبيه | الأداة |
|---|---|---|---|
| البنية التحتية | p95 لزمن انتقال API | > 100 مللي ثانية | Prometheus |
| البنية التحتية | توفر الخدمة | < 99.9% | Grafana |
| مقاييس ML | تحول توزيع التنبؤ | PSI > 0.2 | مخصص |
| مقاييس ML | معدل الميزات المفقودة | > 5% | مخصص |
| المقاييس التجارية | MAPE التنبؤ | > 15% | التقارير التجارية |
| المقاييس التجارية | انحراف الإيرادات | > 10% | التقارير التجارية |
4. اكتشاف انحراف البيانات
(1) PSI (مؤشر استقرار السكان)
▶ مثال: حساب PSI
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:
# Functions defined successfully
(2) اختبار KS وتباعد JS
▶ مثال: مقارنة مقاييس الانحراف المتعددة
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:
# 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
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:
=== Data Drift Detection ===
(2) خط أنابيب إعادة التدريب التلقائي
▶ مثال: إطار اكتشاف الانحراف + إعادة التدريب التلقائي
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:
# Functions defined successfully
❓ أسئلة شائعة
📖 ملخص
- بنية مراقبة من ثلاث طبقات: البنية التحتية (زمن الانتقال/التوفر) → مقاييس ML (توزيع التنبؤ/إحصائيات الميزات) → المقاييس التجارية (MAPE/الإيرادات)
- PSI هو معيار الصناعة لاكتشاف الانحراف: < 0.1 يعني عدم وجود انحراف، 0.1-0.2 يستدعي التحقيق، > 0.2 يتطلب إجراءً
- انحراف البيانات = تغيير في P(X) (توزيع المدخلات)؛ انحراف المفهوم = تغيير في P(Y|X) (العلاقة) - الأخير أكثر خطورة
- خط أنابيب إعادة التدريب التلقائي: اكتشاف الانحراف → فحص الأداء → إعادة التدريب → التحقق A/B → الطرح التدريجي
- التقلب الموسمي مقابل الانحراف: ميز بينهما بالمقارنة من سنة إلى أخرى - الموسمية تعود، الانحراف يستمر
- المراقبة + إعادة التدريب التلقائي هي ما يحافظ على موثوقية نظام ML على المدى الطويل - النشر ليس خط النهاية
📝 تمارين
- أساسي (الصعوبة ⭐): استخدم PSI لاكتشاف تغيير التوزيع بين مجموعتي بيانات، وقارن قيم PSI لحالة عدم الانحراف مقابل انحراف المتوسط بمقدار 10. تلميح: راجع دالة حساب PSI في القسم 4.
- متوسط (الصعوبة ⭐⭐): نفّذ مراقبة من ثلاث طبقات - لبيانات التنبؤ المحاكاة، اكتشف: 1) انحراف PSI؛ 2) تدهور MAPE؛ 3) شذوذ زمن الانتقال (p95 > 100 مللي ثانية). تلميح: ثلاث دوال تحقق مستقلة + تقرير مدمج.
- تحدٍّ (الصعوبة ⭐⭐⭐): نفّذ AutoRetrainPipeline كامل - ولّد بيانات فترة عادية + فترة ترويج، ودرب نموذجًا أوليًا، واكتشف الانحراف وأعد التدريب تلقائيًا، وقارن MAPE قبل وبعد إعادة التدريب، وأخرج تقرير مراقبة كامل. تلميح: راجع إطار إعادة التدريب التلقائي في القسم 5.