الوحدة 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@10 | NDCG@10 | Coverage | Diversity | Cold-user Recall@10 |
|---|---|---|---|---|---|
| Popularity | 0.152 | 0.098 | 0.03 | 0.15 | 0.15 |
| Content | 0.243 | 0.176 | 0.61 | 0.72 | 0.24 |
| iALS | 0.312 | 0.241 | 0.38 | 0.55 | 0.03 |
| Two-Tower | 0.348 | 0.269 | 0.44 | 0.58 | 0.11 |
| Hybride | 0.371 | 0.288 | 0.55 | 0.63 | 0.22 |
الأرقام هنا شاهدة لا معياريّة؛ تختلف من كاتالوغ إلى آخر. القراءة المهمّة:
- Popularity تغطية شبه صفريّة رغم Recall لا بأس به. مشكلة.
- Content هي الوحيدة التي تحلّ Cold-user، لكنّها ضعيفة على المستخدم المُنشِط.
- iALS يفوز على المستخدم الناضج لكنّه ينهار على Cold-user.
- Hybride يجمع نقاط القوّة، وهو ما يعرض في الإنتاج.
خطّة اختبار A/B الحيّ
المقاييس الهوفلاين ليست كافية. نُخطّط اختبارًا حيًّا:
الفرضيّة: النظام الهجين يزيد إتمام الدورات على مدى شهر بنسبة تفوق 5% مقارنةً بالنظام الحاليّ.
البروتوكول: 10% من المتعلّمين يُعرَض عليهم النظام الجديد، 90% النظام الحاليّ (المرجعيّ). العشوائيّة على مستوى المستخدم لا الجلسة، حتّى لا تتلوّث النتائج بتغييرات السلوك بين الجلسات.
المقياس الأساس: نسبة الإتمام خلال 30 يومًا من التسجيل. المقاييس الحماية: زمن الاستجابة (يجب ألّا يتجاوز 200 مس)، متوسّط عدد الدورات المُشاهَدة، تنوّع المحتوى.
المدّة: أسبوعان على الأقلّ، وتقييم إحصائيّ (t-test) قبل قرار النشر.
الشرط الرافض: أيّ انخفاض في مقياس حماية بأكثر من 2% يُوقف الاختبار.