انتقل إلى المحتوى الرئيسي

الوحدة 8 — الخدمة بالدفعات والمتّصلة والتدفّقية

نموذجنا مُدرَّب، مُسجَّل، مُحزَّم. الآن السؤال العملي: كيف يصل التنبّؤ إلى مَن يحتاجه؟ الجواب ليس واحدًا. لكلّ استعمال شكل خدمة يناسبه، والاختيار يبدأ بسؤال واحد: كم من الوقت نستطيع الانتظار للحصول على التوقّع؟

ثلاثة أوضاع، ثلاثة أسئلة

نُميّز ثلاثة أوضاع رئيسية للخدمة:

  • الخدمة بالدفعات (batch): نحسب التوقّعات مسبقًا لمجموعة كاملة (كلّ المشتركين، مثلًا) في تنفيذ دوريّ، ونخزّنها في قاعدة بيانات. عندما نحتاج التوقّع، نستعلم في القاعدة.
  • الخدمة المتّصلة (online): API تستقبل الطلب، تحسب التوقّع فورًا، وتُرجعه. تُخصَّص لكلّ طلب حسبَ خصائصه اللحظيّة.
  • الخدمة التدفّقية (streaming): نستقبل تيّار أحداث (كافكا، مثلًا)، نُحسب التوقّع لكلّ حدث فور وروده، ونُنتج تيّارًا آخر من التوقّعات.

السؤال المُوجِّه: الحدّ الأقصى المقبول لزمن الاستجابة؟

  • إن كان يوم أو ساعة، الدفعات تكفي، وهي أرخص وأبسط.
  • إن كان بضع مئات المللي ثانية، نحتاج خدمة متّصلة.
  • إن كان أقلّ من 50 مللي ثانية مع معدّل عالٍ للأحداث، غالبًا التدفّق أو ذاكرة تخزين مؤقّت متّصلة أقرب.

الحالة الأمثل للدفعات

في مثال التسرّب لدينا، فريق التسويق يستعمل التوقّعات لتصميم حملات احتفاظ أسبوعية. لا يحتاج التوقّع في اللحظة نفسها لكلّ مشترك، بل جدولًا كاملًا صباح كلّ اثنين.

هذه الحالة المثالية للدفعات:

# scripts/batch_predict.py
import pandas as pd
import mlflow

model = mlflow.pyfunc.load_model("models:/churn-classifier@production")
subs = pd.read_sql("SELECT * FROM subscribers WHERE active = TRUE", conn)
subs["churn_score"] = model.predict(subs.drop(columns=["customer_id"]))
subs[["customer_id", "churn_score"]].to_sql("churn_predictions", conn, if_exists="replace")

يُشغَّل هذا النصّ كلّ اثنين في الساعة الثالثة صباحًا عبر Airflow أو cron مُدار. التطبيقات تقرأ من الجدول، لا تنادي أيّ API نموذجًا. فوائد الدفعات:

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

متى نحتاج خدمة متّصلة

خدمة متّصلة (API HTTP) لا غنى عنها عندما تعتمد الميزات على حالة اللحظة:

  • طلب قرض بنكيّ: نحتاج التنبّؤ فور تعبئة النموذج، والقيم مُدخَلة تَوًّا.
  • توصية منتج: تعتمد على المنتج الذي يشاهده المستخدم الآن، والذي أضافه إلى السلّة قبل ثوانٍ.
  • كشف احتيال في وقت الدفع: يجب أن يتمّ التقرير قبل تفويض المعاملة، ولا وقت للانتظار.

الحدّ الأدنى للخدمة المتّصلة: FastAPI أو BentoML يُقدّم النموذج، خلف موزّع أحمال، بتحجيم آلي حسب الحمل. زمن استجابة نموذجيّ: 20 إلى 100 مللي ثانية للاستدلال ذاته، مضافًا إليه زمن الشبكة.

نقطة حرجة: الاختباء وراء ذاكرة تخزين مؤقّت (cache). طلبات كثيرة تتكرّر بنفس المدخلات؛ حفظ آخر ألف توقّع مع مفتاح مُهيَّأ من المدخلات يُقلّل الحمل بشكل كبير، شريطة أن يكون النموذج حتميًّا لمُدخلات متطابقة.

التدفّق وأحداث كافكا

عندما تكون البيانات نفسها تصل بشكل تيّار متواصل، الخدمة التدفّقية أنسب. مثال: نظام قياس عدّاد كهرباء يُرسل قراءة كلّ خمس دقائق. نريد اكتشاف اضطرابات الاستهلاك فور حدوثها.

بنية معياريّة: كافكا لالتقاط الأحداث، وموزّع مثل Kafka Streams أو Flink يقرأ كلّ حدث ويستدعي النموذج ويُنتج توقّعًا على موضوع (topic) آخر. التطبيق يستهلك التوقّعات بنفس الوسيلة.

فوائد التدفّق: زمن نهاية-إلى-نهاية أفضل، ومعالجة حجم هائل من الأحداث دون طاقة استيعاب في اللحظة. تحدّياته: مضاعفة تعقيد البنية، وضمان معالجة كلّ حدث مرّة واحدة بالضبط (exactly-once) صعب تقنيًّا، وتشخيص خلل معيّن في تيّار مباشر أصعب من قاعدة بيانات ساكنة.

تناغم المتغيّرات: نفس الدالّة في التدريب والخدمة

هنا يظهر مصدر أخطاء متكرّر: الميزة تُحسب بطريقة ما في التدريب، وبطريقة أخرى في الإنتاج.

مثال: في التدريب، نحسب average_charge_last_6_months بجمع الفواتير الست الماضية وقسمتها. في API الإنتاج، مبرمج يُنشئ نفس الميزة، لكنّه يُقسم على 6 دائمًا، حتّى لو كان المشترك مسجّلًا منذ شهرين فقط. تدريب النموذج تعلّم أنماطًا لن يراها أبدًا في الإنتاج، لأنّ الميزة الفعلية مختلفة.

الحلّ المؤسّسي: متجر ميزات (feature store، مثل Feast أو Tecton) يُخزّن تعريف كلّ ميزة بشفرة موحّدة، ويوفّرها بنفس القيمة عبر عرضين: بطيء للتدريب (يستعلم عن التاريخ)، سريع للاستدلال (يستعلم عن اللحظة الحاليّة). تكفل الأداة التطابق.

الحدّ الأدنى بدون متجر ميزات: دالّة Python وحيدة (compute_features(df)) تُستعمل حرفيًّا في التدريب والاستدلال. لا تكرار للشفرة. اختبار وحداني يتحقّق أنّ الدالّة تُنتج نفس النتيجة في السياقين.

للتذكير، هذا سيُعمَّق في دورة 33 المخصّصة لمخزن الميزات (link vers cours 33).

قرار الاختيار في جملة واحدة

نلخّص:

  • دفعات إذا كان زمن الاستجابة يُقاس بالساعات؛ الأرخص والأبسط.
  • متّصلة إذا كان يُقاس بالمللي ثانية والمدخلات لحظيّة؛ يستحقّ التعقيد.
  • تدفّقية إذا كانت البيانات نفسها تيّارًا مستمرًّا بحجم كبير وحاجة زمن حقيقي.
تعريف زمن الاستجابة صراحةً

كلمة «سريع» ليست مواصفة. تكتب في العقد التقني: «توقّع 95 بالمائة من الطلبات تحت 80 مللي ثانية، لا انقطاع فوق 500 مللي ثانية». من دون هذه الأرقام، الفريق يبني حلًّا لا يعرف إن كان قد نجح.

في الخلاصة

  • ثلاثة أوضاع للخدمة: دفعات، متّصلة، تدفّقية؛ الاختيار يبدأ بزمن الاستجابة المطلوب.
  • الدفعات أرخص وأبسط، ومناسبة إذا كان التنبّؤ الأسبوعي أو الليلي كافيًا.
  • الخدمة المتّصلة لازمة عندما تعتمد الميزات على حالة اللحظة (طلب قرض، كشف احتيال).
  • تناغم المتغيّرات بين التدريب والإنتاج شرط أساسي؛ متجر ميزات أو دالّة موحّدة، لا نُسختان.