تطوير الويب

كشف انحراف نموذج ML في الإنتاج بـ Evidently

2 دقائق للقراءة

📌 نظرة عامّة: MLOps حديث: من النموذج إلى الإنتاج

لماذا نموذج « مثالي » يَتَدَهور صامتًا

نموذج scoring ائتمان يعمل بـ 87% AUC طوال 3 أشهر. في الشهر الرابع، نسبة التخلّف الفعلية ترتفع من 4.1% إلى 6.8% دون أن يلحظ أحد. السبب: توزيع دخل المُتَقَدِّمين الجدد انزلق، والنموذج المُدَرَّب على السكّان القدامى يُبالغ في تقدير الملفّات الخطرة. يُظهر هذا الدليل كيف نُسَلِّح هذا النوع من pipelines مع Evidently AI 0.7 — حساب data drift، concept drift، ودرت الهدف، توليد تقرير HTML، تصدير metrics إلى Prometheus وتشغيل تنبيهات Slack عبر Alertmanager.

نموذج تعلّم آلي يتعلّم علاقة بين features دخول وهدف خرج على dataset مرجع. في الإنتاج، شيئان يتحرّكان دائمًا: بيانات الدخل (المستخدمون يتغيّرون، الفصول تتعاقب، حملة تسويقية تُغَيِّر السلوك) والعلاقة بين هذه البيانات والهدف (مُنافس جديد يُخَفِّض القدرة الشرائية، إصلاح ضريبي يُزيح الدخل). دون تسليح، لا ترى التدهور إلّا حين ينفجر KPI، أي متأخّرًا.

كشف الانحراف يقارن دوريًّا توزيع البيانات الأخيرة بتوزيع dataset التدريب. إن تجاوز الفارق عتبة، نُشَغِّل تنبيهًا. Evidently AI، مشروع open source مَصون نشطًا، يُؤَتمت هذا الحساب.

Data drift، concept drift، prediction drift: ثلاثة أشياء مختلفة

المصطلحات الثلاثة تدور كمترادفات، لكنّها تُشير لظواهر متمايزة.

Data drift (أو covariate shift): توزيع features الدخل يتغيّر بين المرجع والإنتاج. مثال: نسبة المُتَقَدِّمين دون 25 سنة تنتقل من 18% إلى 34%. الأسهل قياسًا لأنّه لا يتطلّب الهدف الفعلي. Evidently يختبر كلّ عمود فُرادى بـ اختبار إحصائي مناسب لنوعه.

Concept drift: العلاقة بين features والهدف تتغيّر، حتى لو بقيت توزيعات الدخل مطابقة. مثال: دخل شهري كان يتنبّأ بمخاطرة منخفضة قبل سنتَين، لكن بعد ارتفاع كلفة الإيجار، نفس الدخل يتنبّأ بمخاطرة عالية. concept drift يتطلّب ground truth.

Prediction drift: توزيع تنبّؤات النموذج يتغيّر. مراقبته تُتيح الردّ من اليوم الأوّل دون انتظار ground truth.

المتطلّبات

Python 3.10 كحدّ أدنى (3.11 أو 3.12 موصى بهما)، Evidently 0.7.x، pandas، scikit-learn. venv نظيف يتفادى تعارضات مع نسخ قديمة لـ Evidently. للمراقبة، تحتاج Prometheus قابل للـ push (عبر Pushgateway) أو scrape، وAlertmanager مع webhook Slack.

الخطوة 1 — تثبيت Evidently وتحضير البيئة

python -m venv .venv-drift
source .venv-drift/bin/activate
pip install --upgrade pip
pip install "evidently>=0.7,<0.8" pandas scikit-learn prometheus_client
python -c "import evidently; print(evidently.__version__)"

الخرج يجب عرض 0.7.21 أو نحوه. إن رأيت 0.4.x أو 0.3.x، أنت على API قديم وimports هذا الدليل لن تعمل.

الخطوة 2 — تحضير dataset مرجع وdataset جارٍ

import pandas as pd
from sklearn.datasets import fetch_openml

adult = fetch_openml(name="adult", version=2, as_frame=True)
df = adult.frame.dropna()

# مرجع: عيّنة "سليمة" بـ 5000 سطر
reference = df.sample(n=5000, random_state=42)

# الجاري: نحوّر لخلق انحراف مرئي
current = df[df["age"] > 45].sample(n=2000, random_state=7)

print(reference.shape, current.shape)
print("متوسّط عمر reference :", reference["age"].mean())
print("متوسّط عمر current   :", current["age"].mean())

الخرج المتوقّع يُظهر فرقًا واضحًا في متوسّط العمر: 38.6 للمرجع مقابل 55.2 للجاري. هذه الإشارة التي سيُكَمِّمها Evidently إحصائيًّا.

الخطوة 3 — تشغيل تقرير Report بـ preset Data Drift

API 0.7 يكشف كائن Report يأخذ قائمة metrics أو presets. preset مجموعة metrics متماسكة — DataDriftPreset يُغَطّي drift كلّ أعمدة DataFrame باختيار الاختبار الإحصائي المناسب لكلّ منها.

from evidently import Report
from evidently.presets import DataDriftPreset

report = Report([DataDriftPreset()], include_tests=True)
snapshot = report.run(current_data=current, reference_data=reference)

snapshot.save_html("drift_report.html")

include_tests=True يُضيف شروط pass/fail تلقائيًّا. الملفّ drift_report.html تقرير مستقلّ يُفتَح في أيّ متصفّح. إشارة النجاح: تجد في الأعلى بلوك "Dataset Drift" بحالة Detected/Not Detected.

الخطوة 4 — قراءة تقرير HTML (drift_share وdrift لكلّ feature)

التقرير يُقرأ على ثلاثة مستويات. في القمّة، drift_share: نسبة الأعمدة التي اكتشف الاختبار درت معنويًّا. افتراضيًّا، Evidently يُعلن dataset كـ "drifting" إن دَرَتْ أكثر من 50% من الأعمدة.

في المستوى الوسيط، جدول يُدرج كلّ عمود: النوع المكشوف، الاختبار المُستعمَل، قيمة الإحصائية، والحكم drift/no drift. للأعمدة الرقمية بأكثر من 1000 ملاحظة، Evidently يستعمل افتراضيًّا مسافة Wasserstein المُعَلَّبَة؛ للفئوية، Chi² أو Jensen-Shannon. تحت 1000 ملاحظة، Kolmogorov-Smirnov يأخذ المهمّة على الأعمدة الرقمية.

result = snapshot.dict()

for metric in result["metrics"]:
    if "DriftedColumnsCount" in metric.get("metric_id", ""):
        print("Colonnes en dérive :", metric["value"])
    if "DataDriftTable" in metric.get("metric_id", ""):
        for col, info in metric["value"].get("drift_by_columns", {}).items():
            if info.get("drift_detected"):
                print(f"  - {col} ({info.get('stattest_name')}) : score={info.get('drift_score'):.3f}")

الخطوة 5 — تهيئة عتبات مخصَّصة

from evidently.presets import DataDriftPreset

preset = DataDriftPreset(
    method="psi",        # يفرض Population Stability Index لكلّ الأعمدة
    threshold=0.2,       # عتبة PSI: > 0.2 = drift معنوي
    drift_share=0.3,     # dataset في drift إن > 30% من الأعمدة دَرَتْ
)

report = Report([preset], include_tests=True)
snapshot = report.run(current_data=current, reference_data=reference)

لماذا PSI بدل Wasserstein؟ PSI الاصطلاح التاريخي لصناعة التمويل. مراحله مُتَذَكَّرة: < 0.1 مستقرّ، 0.1 إلى 0.2 انتباه، > 0.2 drift معنوي. Wasserstein لا مرحلة شاملة له ويتطلّب benchmark داخليًّا.

الخطوة 6 — قياس درت الهدف (regression أو classification)

from evidently import Report, DataDefinition
from evidently.presets import DataDriftPreset, ClassificationPreset

schema = DataDefinition(
    classification=[{"target": "income", "prediction_labels": "prediction"}]
)

report = Report(
    metrics=[
        DataDriftPreset(columns=["age", "hours-per-week", "education-num"]),
        ClassificationPreset(),
    ],
    include_tests=True,
)
snapshot = report.run(
    current_data=current,
    reference_data=reference,
    data_definition=schema,
)
snapshot.save_html("drift_and_target.html")

لـ regression، استبدل ClassificationPreset بـ RegressionPreset. التقرير يحوي 3 blocs: data drift، توزيع الهدف، وmetrics الأداء (accuracy / RMSE).

الخطوة 7 — دمج الاختبارات في CI/CD

import sys

result = snapshot.dict()
tests  = result.get("tests", [])
failed = [t for t in tests if t.get("status") == "FAIL"]

for t in failed:
    print(f"FAIL : {t.get('name')} — {t.get('description')}")

if failed:
    print(f"\n{len(failed)} test(s) en échec, déploiement bloqué.")
    sys.exit(1)
print("Tous les tests passent, déploiement autorisé.")

مدمج في GitHub Action أو GitLab CI، هذا script يفشل بكود 1 فور تجاوز عتبة، فيتوقّف pipeline قبل الترقية.

الخطوة 8 — كشف metrics نحو Prometheus

from prometheus_client import CollectorRegistry, Gauge, push_to_gateway

registry = CollectorRegistry()

drift_share_gauge = Gauge(
    "ml_drift_share",
    "Proportion de colonnes en dérive sur le dataset courant",
    ["model", "env"],
    registry=registry,
)
drifted_count_gauge = Gauge(
    "ml_drifted_columns_count",
    "Nombre absolu de colonnes en dérive",
    ["model", "env"],
    registry=registry,
)

result = snapshot.dict()
for metric in result["metrics"]:
    mid = metric.get("metric_id", "")
    if "DriftedColumnsCount" in mid:
        drifted_count_gauge.labels(model="credit-scoring", env="prod").set(
            metric["value"].get("count", 0)
        )
        drift_share_gauge.labels(model="credit-scoring", env="prod").set(
            metric["value"].get("share", 0.0)
        )

push_to_gateway("pushgateway.monitoring.svc:9091", job="evidently-drift", registry=registry)
print("Métriques poussées vers Prometheus.")

الـ Pushgateway مُصَمَّمة لـ jobs batch قصيرة. لـ drift محسوب كلّ 15 دقيقة، مثالي.

الخطوة 9 — ربط Alertmanager وSlack

# prometheus-rules.yaml
groups:
  - name: ml-drift
    interval: 1m
    rules:
      - alert: HighDataDrift
        expr: ml_drift_share{env="prod"} > 0.3
        for: 30m
        labels:
          severity: warning
          team: ml-platform
        annotations:
          summary: "Drift élevé détecté sur {{ $labels.model }}"
          description: "{{ $value | humanizePercentage }} des colonnes dérivent."

      - alert: CriticalDataDrift
        expr: ml_drift_share{env="prod"} > 0.6
        for: 10m
        labels:
          severity: critical
          team: ml-platform
        annotations:
          summary: "Drift CRITIQUE sur {{ $labels.model }} — re-training à envisager"
# alertmanager.yaml
route:
  receiver: slack-warnings
  group_by: [alertname, model]
  routes:
    - match:
        severity: critical
      receiver: slack-critical
      continue: true

receivers:
  - name: slack-warnings
    slack_configs:
      - api_url: https://hooks.slack.com/services/T0000/B0000/XXXXXXXX
        channel: "#ml-monitoring"
        send_resolved: true
  - name: slack-critical
    slack_configs:
      - api_url: https://hooks.slack.com/services/T0000/B0001/YYYYYYYY
        channel: "#ml-incidents"
        send_resolved: true
        title: "DRIFT CRITIQUE : {{ .CommonLabels.model }}"

الخطوة 10 — workflow ردّ كامل

الكشف لا يكفي: يجب تقرير الفعل. ثلاثة مستويات.

المستوى 1 — إعلام صامت. drift_share > 10% لكن < 30%. نُسَجِّل metric، نعرضها على dashboard Grafana، لكن لا تنبيه.

المستوى 2 — تنبيه بشري. drift_share > 30% أو feature حرجة (دخل، عمر، مبلغ معاملة) دَرَتْ منفردة بـ PSI > 0.25. رسالة Slack تصل، data scientist يفحص HTML، يُحَدِّد السبب ويُقَرِّر.

المستوى 3 — re-training تلقائي. drift_share > 60% والأداء على ground truth الأخيرة انخفض أكثر من 5 نقاط. pipeline يُشَغِّل تلقائيًّا job إعادة تدريب. الأتمتة الكاملة مُربحة فقط إن كان ground truth سريعًا (نقرة، شراء، ردّ < 24 ساعة).

المرجع نفسه يجب صيانته: كلّ 6 أشهر، أو بعد كلّ re-training، نستبدل dataset المرجع بنافذة حديثة سليمة.

أخطاء شائعة

العَرَض السبب التصحيح
ImportError على evidently.report توثيق قديم (≤ 0.6.7) مع نسخة 0.7+ استبدل بـ from evidently import Report
drift مكشوف على كلّ الأعمدة datasets صغيرة جدًّا زد إلى ≥ 1000 سطر، أو افرض method="psi"
لا drift رغم تغيّر واضح reference واسع جدًّا أعد معايرة reference على نافذة ضيّقة
تقرير HTML بطيء (> 30 ث) أعمدة كثيرة (200+) اختر الأعمدة الحرجة عبر DataDriftPreset(columns=[...])
Pushgateway لا يكشف المقياس job باسم مطابق يطغى أضف label instance فريدًا
تنبيهات Slack في حلقة for: 0m غائب ضع دائمًا for: 10m أو أكثر
concept drift دائمًا "لا بيانات" لا ground truth في dataset الجاري نَفِّذ التقاط label فعلي قبل مراقبة concept drift

مصادر رسمية

  • توثيق Evidently AI — مرجع كامل لـ API 0.7
  • توثيق DataDriftPreset
  • مستودع GitHub Evidently
  • دليل Prometheus Pushgateway
  • توثيق Alertmanager

الأدلّة المرتبطة

FAQ

أيّ تواتر حساب drift في الإنتاج؟ كلّ 15 إلى 60 دقيقة للـ data drift وprediction drift على حجم متوسّط. دون 1000 سطر/نافذة، اجمع على نافذة أطول. للـ concept drift، إيقاع أسبوعي أو شهري.

هل دائمًا DataDriftPreset، أم metrics واحدًا واحدًا؟ الـ preset يكفي 80% من الحالات. اختيار metric metric مفيد فوق 50 features.

PSI دائمًا الخيار الصحيح؟ متين على توزيعات مستمرّة وقابل للتفسير لغير data-scientists. يصير غير مستقرّ على توزيعات شديدة الانحراف حيث Wasserstein أو Jensen-Shannon أفضل.

كيف نتفادى أن يُشَغِّل حدث موسمي معروف تنبيهات؟ وَسِّع reference ليُغَطّي التباين الموسمي (سنة منزلقة)، أو احفظ عدّة references (يوم أسبوع، شهر).

أيمكن استعمال Evidently على بيانات غير جدولية (صور، نصّ)؟ نعم جزئيًّا. Evidently 0.7 يدعم drift على embeddings — تحسبها بـ sentence-transformers أو CLIP وتُمَرِّر المصفوفة لـ EmbeddingsDriftMetric.

كيف نُؤَرِّخ التقارير بدل استبدالها؟ Evidently 0.7 يعرض Workspace محلّيًّا. تُنشئ project، وكلّ report.run() يُنتج snapshot JSON يُضاف عبر project.add_run().

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité