الوحدة 7 — البداية الباردة: مستخدمون وعناصر جدد
كلّ ما بنيناه حتّى الآن يفترض تاريخًا سابقًا من التفاعلات. حين لا يوجد هذا التاريخ، تفشل التصفية التعاونيّة بلا استثناء، ويعجز حتّى النموذج العميق ذو البُرجَين عن إنتاج شيء أفضل من الصدفة. هذه المشكلة، التي تُسمّى البداية الباردة، لا تُحلّ بخوارزميّة واحدة، بل بمزيج من التصاميم المدروسة.
ثلاث حالات مختلفة، لا واحدة
كثيرًا ما تُقدَّم البداية الباردة على أنّها مشكلة واحدة، لكنّها ثلاث في الواقع، ولكلّ منها حلّ مختلف.
عنصر جديد: دورة أُضيفت الأسبوع الماضي، لم يُقيّمها أحد. المشكلة أنّها لن تظهر في أيّ توصية قائمة على التصفية التعاونيّة، فلا يمكن أن يقيّمها أحد، فتبقى مخفيّة إلى الأبد. حلقة مفرغة.
مستخدم جديد: متعلّم سجّل في المنصّة اليوم. لا نعرف عن ذوقه شيئًا، فأيّ توصية شخصيّة نصنعها مبنيّة على العدم.
نظام جديد: منصّة انطلقت للتوّ، فلا تفاعلات إطلاقًا. حالة أشدّ حدّة، تُحلّ عادةً بترحيل جزئيّ من نظام مشابه (مثلًا الاستعانة بـMovieLens لتدريب نموذج أوّليّ لمنصّة أفلام جديدة).
بداية العنصر الجديد: المحتوى
الحلّ الأصفى هنا هو التصفية بالمحتوى من الوحدة الرابعة. عنصر جديد له وصف، فله تضمين، فيمكن ترتيبه فورًا مقابل بروفيلات المستخدمين. لكنّ هذا لا يكفي وحده لكسر الحلقة المفرغة.
التصميم المدروس: نحجز جزءًا صريحًا من قائمة التوصية للعناصر الجديدة. مثلًا في العشرة الأوائل، 8 من التصفية التعاونيّة و2 من العناصر الجديدة التي تشبه ذوق المستخدم. هذا يضمن أنّ كلّ عنصر جديد يحصل على تعرّض أوّل، فيتراكم عدد التقييمات، فينتقل تدريجيًّا إلى نظام التصفية التعاونيّة الاعتيادي.
كثير من المنصّات تعرض شارة جديد على هذه العناصر، لأنّ الجدّة نفسها إشارة إيجابيّة للمستخدم النَّهِم.
def melange_avec_nouveaux(recos_cf, nouveaux_items, top_k=10, part_nouveaux=0.2):
"""يخلط عناصر جديدة في القائمة النهائيّة."""
nb_nouveaux = int(top_k * part_nouveaux)
nb_cf = top_k - nb_nouveaux
return recos_cf[:nb_cf] + nouveaux_items[:nb_nouveaux]
القاعدة الحاسمة: لا تأخذ العناصر الجديدة كلّها من العناصر الجديدة تلقائيًّا. لا يتحمّل المستخدم قائمة عشرة عناصر لم يسمع بها. النسبة العمليّة بين 10 و30 بالمئة، تُضبَط بالاختبار الحيّ.
بداية المستخدم الجديد: استبيان قصير
لا نستطيع أن نطلب من كلّ متعلّم جديد تقييم مئة دورة قبل استعمال المنصّة. لكن يمكن أن نطلب منه اختيار خمسة عناصر تعجبه من قائمة مختارة بذكاء. هذه المقاربة يستعملها Netflix عند التسجيل («اختر ثلاثة أفلام أحببتها») وSpotify («اختر خمسة فنّانين»).
اختيار العناصر المعروضة في الاستبيان ليس عشوائيًّا. نُريد عناصر تنقسم بها الآراء: عنصر يحبّه الجميع لا يخبرنا شيئًا. المقياس المستعمَل هو entropie التقييمات: نُختار العناصر ذات التوزيع الأكثر تشتّتًا.
import numpy as np
def entropie_item(ratings_par_item):
"""يحسب عدم استقرار الآراء على العنصر."""
counts, _ = np.histogram(ratings_par_item, bins=[1, 2, 3, 4, 5, 6])
probs = counts / (counts.sum() + 1e-9)
return -(probs * np.log(probs + 1e-9)).sum()
# تحدّد أفضل 20 عنصرًا للاستبيان
items_informatifs = sorted(
range(nb_items),
key=lambda i: -entropie_item(ratings_par_item[i])
)[:20]
بعد الاستبيان، نُطبّق بروفيل المحتوى من الوحدة الرابعة (متوسّط تضمينات العناصر المختارة). المستخدم الجديد يحصل على توصيات معقولة من أوّل جلسة.
الشعبيّة والاستكشاف
يبقى مسار لمستخدم لم يشأ إجابة الاستبيان: الشعبيّة. توصية أعلى عشرة عناصر مُقيَّمة في المنصّة توصية معقولة إحصائيًّا: العناصر الشعبيّة تستحقّ شعبيّتها في المتوسّط، والاحتمال أن يعجب المستخدم أحدها مرتفع.
لكنّ الشعبيّة الخالصة ليست خيارًا نهائيًّا. تُنشئ حلقة تغذية راجعة تُثري الغنيّ (الشعبيّ يُعرَض أكثر فيصير أشعب) وتُفقر الفقير. الحلّ: الاستكشاف المحسوب.
Epsilon-Greedy
الشكل الأبسط لسياسة الاستكشاف اسمه ε-Greedy، مستعار من التعلّم بالتعزيز. في كلّ توصية:
- باحتمال : نعرض أفضل توقّع للنموذج (استغلال).
- باحتمال : نعرض عنصرًا عشوائيًّا (استكشاف).
عادةً بين 0.05 و0.15. أقلّ من ذلك: النموذج لا يكتشف شيئًا جديدًا. أكثر من ذلك: تجربة المستخدم تصبح عشوائيّة بشكل ملحوظ.
import random
def recommandation_epsilon(top_recos, catalogue_complet, epsilon=0.1):
"""تُدخل عناصر عشوائيّة بنسبة epsilon من القائمة."""
n = len(top_recos)
nb_explore = int(round(n * epsilon))
if nb_explore == 0:
return top_recos
# تختار عناصر لم تظهر أصلًا في القائمة الأصليّة
hors_top = [x for x in catalogue_complet if x not in top_recos]
random.shuffle(hors_top)
positions_explore = random.sample(range(n), nb_explore)
resultat = list(top_recos)
for pos, item in zip(positions_explore, hors_top[:nb_explore]):
resultat[pos] = item
return resultat
ه ذه الاستراتيجيّة تحلّ المشكلة العميقة: كيف نعرف أنّ عنصرًا سيروق لمستخدم إذا لم نعرضه عليه أبدًا؟ إجابة ε-Greedy: نعرضه أحيانًا لنتعلّم.
Thompson Sampling
الشكل الأدقّ رياضيًّا هو أخذ عيّنات ثومبسون، الذي يعامل كلّ عنصر بوصفه ذراعًا في مسألة اللصوص متعدّدة الأذرع. لكلّ عنصر توزيع Beta على احتمال «سيعجب المستخدم»، ونُحدّث المعلمات بكلّ نقرة ومتجاوز. عند التوصية، نُسحب عيّنة من كلّ توزيع، ونُوصي بأعلى العيّنات.
المزيّة على ε-Greedy: الاستكشاف موجّه نحو العناصر التي فيها عدم يقين حقيقيّ، لا نحو عناصر جرّبناها كثيرًا. عمليًّا، Thompson Sampling يستكشف ذكيًّا حين تكون البيانات قليلة، ويستغلّ عندما تنضج.
قياس معالجة البداية الباردة
الخطأ الشائع تقييم كلّ النظام بـmoyenne واحدة تُخفي مشكلة البداية الباردة. المقاييس المفيدة:
- الاستدعاء عل ى المستخدمين الجُدد فقط (أقلّ من 5 تفاعلات).
- متوسّط الظهور لكلّ عنصر (لا يوجد عنصر لم يُعرض إطلاقًا).
- التغطية على العناصر الجديدة: نسبة العناصر أُضيفت خلال الشهر الماضي والتي ظهرت في توصية واحدة على الأقلّ.
مقياس التغطية الثالث كثيرًا ما يُهمَل ويكشف مشكلات كارثيّة: نظام يعطي 85% استدعاء على مقياس عامّ قد يعرض 3% فقط من العناصر الجديدة.
لا تجعل النظام يتعلّم على البيانات التي يولّدها هو نفسه دون تصحيح. إن كنت لا تعرض إلّا الشعبيّ، فالتقييمات القادمة كلّها ستقوى الشعبيّ. الاستكشاف ليس ترفًا، بل شرط بقاء حياة النظام على المدى الطويل.
الخلاصة
- البداية الباردة ثلاث مشكلات: عنصر جديد، مستخدم جديد، و