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

الوحدة 10 — المشروع: محرّك توصية مُقيَّم

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

مواصفات المشروع

نبني نظام توصية لمنصّة دورات على الإنترنت لديها:

  • 500 دورة (كلّ منها بعنوان ووصف طويل من 100 إلى 500 كلمة وقائمة كلمات مفتاحيّة ومستوى).
  • 50000 متعلّم مُسجَّل.
  • سجلّ تفاعل يمتدّ 18 شهرًا: تسجيلات (implicit)، تقييمات نجميّة (explicit) على نحو 5% من التسجيلات، دقائق المشاهدة، إتمامات.

المخرَج المطلوب: لكلّ متعلّم، قائمة أعلى 10 توصيات مصحوبة بشرح موجز، وتقرير مقاييس على مجموعة اختبار.

البنية العامّة

projet/
data/
interactions.parquet
courses.parquet
users.parquet
src/
split.py # التقسيم الزمنيّ
baselines.py # الشعبيّة والمحتوى
mf.py # SVD وiALS
two_tower.py # النموذج ذو البُرجَين
metrics.py # NDCG, Recall, Coverage
hybride.py # الخلط النهائيّ
evaluate.py # لوحة النتائج

كلّ ملفّ يعرّف صنفًا واحدًا بواجهة موحّدة: fit(X_train) وrecommend(user_id, top_k=10). هذا الانضباط يُتيح استبدال نموذج بآخر دون تغيير كود التقييم.

التقسيم الزمنيّ الصارم

هذه النقطة قاتلة للأنظمة الأكاديميّة التي تفشل في الإنتاج. لا تُقسَّم البيانات عشوائيًّا؛ نُقسَّم بتاريخ.

import pandas as pd

df = pd.read_parquet("data/interactions.parquet")
df["date"] = pd.to_datetime(df["timestamp"], unit="s")

# 15 mois pour entraîner, 2 mois pour valider, 1 mois pour tester
seuil_val = df["date"].max() - pd.Timedelta(days=90)
seuil_test = df["date"].max() - pd.Timedelta(days=30)

train = df[df["date"] < seuil_val]
val = df[(df["date"] >= seuil_val) & (df["date"] < seuil_test)]
test = df[df["date"] >= seuil_test]

print(f"Train: {len(train):,}, Val: {len(val):,}, Test: {len(test):,}")

نتحقّق أنّ كلّ تفاعل في test أحدث من كلّ تفاعل في train. المستخدمون الجُدد في test الذين لا يظهرون في train يشكّلون تلقائيًّا مجموعة اختبار البداية الباردة — كنز لتقييم مقاربة الوحدة 7.

الخطّ المرجعيّ متعدّد الطبقات

قبل بناء أيّ نموذج معقّد، نبني ثلاثة أنظمة مرجعيّة يجب على كلّ نموذج أدقّ أن يتغلّب عليها:

المرجعيّ 1: الشعبيّة. لكلّ مستخدم، توصيته هي أعلى 10 دورات مُقيَّمة في مجموعة التدريب. لا تخصيص إطلاقًا. صحيّ أن يعطي هذا النظام Recall@10 حوالي 15% على منصّة الدورات.

المرجعيّ 2: التصفية بالمحتوى. تضمين وصف الدورات بـE5، بروفيل المستخدم كمتوسّط تضمينات دوراته السابقة. أعلى 10 توصيات هي الأقرب كوسينوسيًّا. سيعطي حوالي 22-25%.

المرجعيّ 3: iALS. نموذج ضمنيّ مباشر على مصفوفة (متعلّم × دورة) بقيم = دقائق المشاهدة. حوالي 28-32%.

نُقدّم النتائج في جدول واحد. أي نموذج نقترحه أعقد من هذه الثلاثة يجب أن يفوز بفارق ملموس، وإلّا فلا يستحقّ الإنتاج.

النموذج المرشّح: هجين ذو بُرجَين

بعد الخطّ المرجعيّ، نُدرّب نموذج البُرجَين (الوحدة 6). ثم نُنشئ نظامًا هجينًا (الوحدة 5) يجمع نتائجه مع نتائج iALS. الوزن يُضبَط على مجموعة التحقّق.

class HybrideFinal:
def __init__(self, deux_tours, ials, contenu, alpha=0.5, beta=0.3):
self.dt = deux_tours
self.ials = ials
self.contenu = contenu
self.alpha = alpha
self.beta = beta

def recommend(self, user_id, top_k=10):
candidats = self.dt.top_k(user_id, k=200) # récupération rapide
scores = {}
for item in candidats:
s_dt = self.dt.score(user_id, item)
s_ials = self.ials.score(user_id, item)
s_content = self.contenu.score(user_id, item)
scores[item] = (
self.alpha * s_dt
+ self.beta * s_ials
+ (1 - self.alpha - self.beta) * s_content
)
return sorted(scores, key=lambda x: -scores[x])[:top_k]

المرشّحون يأتون من نموذج البُرجَين (استرجاع سريع)، ثمّ يُرتَّبون بالخلط الموزون. الشرح للمستخدم يذكر النموذج الغالب على كلّ توصية.

لوحة المقاييس

نُنتج جدولًا واحدًا يجمع كلّ المقاييس على مجموعة الاختبار:

النظامRecall@10NDCG@10CoverageDiversityCold-user Recall@10
Popularity0.1520.0980.030.150.15
Content0.2430.1760.610.720.24
iALS0.3120.2410.380.550.03
Two-Tower0.3480.2690.440.580.11
Hybride0.3710.2880.550.630.22

الأرقام هنا شاهدة لا معياريّة؛ تختلف من كاتالوغ إلى آخر. القراءة المهمّة:

  • Popularity تغطية شبه صفريّة رغم Recall لا بأس به. مشكلة.
  • Content هي الوحيدة التي تحلّ Cold-user، لكنّها ضعيفة على المستخدم المُنشِط.
  • iALS يفوز على المستخدم الناضج لكنّه ينهار على Cold-user.
  • Hybride يجمع نقاط القوّة، وهو ما يعرض في الإنتاج.

خطّة اختبار A/B الحيّ

المقاييس الهوفلاين ليست كافية. نُخطّط اختبارًا حيًّا:

الفرضيّة: النظام الهجين يزيد إتمام الدورات على مدى شهر بنسبة تفوق 5% مقارنةً بالنظام الحاليّ.

البروتوكول: 10% من المتعلّمين يُعرَض عليهم النظام الجديد، 90% النظام الحاليّ (المرجعيّ). العشوائيّة على مستوى المستخدم لا الجلسة، حتّى لا تتلوّث النتائج بتغييرات السلوك بين الجلسات.

المقياس الأساس: نسبة الإتمام خلال 30 يومًا من التسجيل. المقاييس الحماية: زمن الاستجابة (يجب ألّا يتجاوز 200 مس)، متوسّط عدد الدورات المُشاهَدة، تنوّع المحتوى.

المدّة: أسبوعان على الأقلّ، وتقييم إحصائيّ (t-test) قبل قرار النشر.

الشرط الرافض: أيّ انخفاض في مقياس حماية بأكثر من 2% يُوقف الاختبار.

الحدود التي يجب الاعتراف بها

كلّ نظام له حدود. الاعتراف بها علنًا يُميّز مهندسًا ناضجًا:

  • لا يوجد تقييم لجودة الشرح المرفق بكلّ توصية.
  • الحلقة الخبيثة لم تُصحَّح كلّيًّا؛ IPS يحتاج بيانات عشوائيّة نظيفة، لم نجمعها إلّا لعيّنة قصيرة.
  • البداية الباردة للنظام كلّه لم تُختبَر (منصّتنا موجودة منذ 18 شهرًا).
  • العدل بين الفئات: لم نتحقّق ممّا إذا كانت التوصية تعمل بنفس الجودة عبر بلدان مختلفة أو مستويات اجتماعيّة مختلفة.

ما يجب تقديمه

للإنتاج:

  1. رمز Python نظيف قابل لإعادة التشغيل، بمتطلّبات مثبّتة (requirements.txt).
  2. دفتر Jupyter واحد يعيد إنتاج كلّ الأرقام في جدول المقاييس.
  3. تقرير من صفحتَين يعرض النتائج بجدول واحد، لوحة رسم واحدة، وسبع بنود من الحدود.
  4. خطّة اختبار A/B مفصّلة.

الوقت الواقعيّ لهذا المشروع: 40 إلى 60 ساعة. لا تُقصّر منه؛ هو ما يميّز CV ذو مشروع حقيقيّ عن CV آخر.

الخلاصة

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

الوحدة التالية: المراجعة والاختبار، حيث نُلخّص القرارات الحرجة ونُعلن الاختبار من 40 سؤالًا.