بناء نظام اقتراحات Recommender System
دليل للمبتدئين لبناء نظام اقتراحات مع الشرح خطوة بخطوة لكيفية بناء نظام تصفية بناءاً على المحتوى،كي يقترح أفلام للمستخدم لمشاهدتها
[رابط الموضوع الأصلي هنا]
نظم الاقتراحات واحدة من أبرز الأمثلة لاستخدامات تعلم الآلة في حياتنا اليومية. هذه النظم تحدد ماذا يظهر لك في صفحة الفيسبوك الرئيسية الخاصة بك وكيف يتم تقديم المنتجات في أمازون وأي الفيديوهات يتم اقتراحها لك عندما تشاهد نيتفلكس. بالإضافة هناك استخدامات أخرى متعددة. لكن ما هي نظم الاقتراحات وكيف تعمل؟ هذه التدوينة هي الأولى في سلسلة تدوينات لاستكشاف بعض التقنيات الشائعة لبناء نظام اقتراحات و كيفية تطبيقها.
ما هو نظام الاقتراحات؟
هو نظام مبني على نموذج تصفية المعلومات وترتيبها وتقييمها للمستخدم. بشكل عام هناك طريقتان للترتيب:
- تصفية بناءاً على المحتوى (Content-based filtering): كل العناصر المقترحة مبنية على درجة التشابه بين كل وحدة ووحدة وتفضيل المستخدم.
- تصفية تعاونية (Collaborative filtering): كل العناصر المقترحة للمستخدم مبنية على تفضيلات مستخدمين آخرين لديهم عمليات سابقة وخواص مشابهة.
المعلومات المستعملة في التصفية التعاونية قد تكون مباشرة حيث يقيّم المستخدمون كل عنصر، أو تكون غير مباشرة حيث يتم استنباط تفضيلات المستخدم بناءاً على تصرفاته (مشتريات، مشاهدات، و غيره). أفضل نظم الاقتراحات تستخدم طرق هجينة تدمج طريقتي التصفية المذكورة أعلاه.
مجموعة بيانات MovieLens
لجعل النقاش أكثر وضوحاً، دعونا نركز على بناء نظام اقتراحات باستخدام مثال معين. المجموعة البحثية GroupLens من جامعة مينيسوتا وفرت مجموعة بيانات اسمها MovieLens. مجموعة البيانات هذه تحتوي على 20 مليون تقييم تقريباً لـ 27 ألف فلم من 138 ألف مستخدم. بالإضافة أنها تحتوي على نوع وتاريخ كل فلم. سنستخدم مجموعة البيانات هذه.
نظام اقتراحات بسيط بناءاً على المحتوى (Simple Content-based Filtering)
لنبني نظام اقتراحات بسيط مبني على المحتوى (باستخدام الشبه بين العناصر) في اقتراح أفلام للمشاهدة. أولاً نحتاج تحميل مجموعة بيانات MovieLens وتشفير نوع الأفلام:
import pandas as pd
import numpy as np
movies = pd.read_csv("movies.csv")
movies = movies.join(movies.genres.str.get_dummies("|"))
خاصية “النوع” للفلم تحتوي على أكثر من نوع مفصولة ب (“|”). السطر الأخير في الأعلى يضيف عمود في الجدول لكل نوع و يضع الرقم 1 لذلك المدخل اذا كان النوع ينطبق على الفلم و 0 غير ذلك.
الآن لنصنع بعض الاقتراحات بناءاً على تشابه العناصر باستخدام هذه الرموز. إحدى الطرق المنتشرة لقياس التشابه للبيانات النوعية (مثل نوع الفلم) هي تشابه جتا (cosine similarity). لأي عنصرين i و j يمثل تشابه جتا بين العنصرين بجتا الزاوية بين i و j حيث أن i و j ممثلان كمتجهات في فضاء الخصائص. تذكر بأنه يمكننا الحصول على جتا الزاوية من خلال الضرب الداخلي لهذه المتجهات:
كمثال للنظر لهذين الفلمين
$i := $ Toy Story (genre tags “Adventure”, “Animation”, “Children”, “Comedy”, “Fantasy”)
$j := $ Jumanji (genre tags “Adventure”, “Children”, and “Fantasy”)
الضرب الداخلي بين الفلمين يساوي 3 (لأن هناك 3 رموز متشابهة بين الفلمين).
و بذلك نجد أن تشابه جتا بين الفلمية يساوي
يمكننا حساب تشابه جتا لكل العناصر في مجموعة البيانات كالآتي:
from sklearn.metrics.pairwise import cosine_similarity
cos_sim = cosine_similarity(movies.iloc[:,3:])
أول فلم في مجموعة البيانات هو فلم Toy Story. لنرى ما هي الأفلام المشابهة له:
toystory_top5 = np.argsort(cos_sim[0])[-5:][::-1]
Genres | MovieID |
Adventure,Animation,Children,Comedy,Fantasy | Toy Story (1995) |
Adventure,Animation,Children,Comedy,Fantasy | Turbo (2013) |
Adventure,Animation,Children,Comedy,Fantasy | Monsters, Inc. (2001) |
Adventure,Animation,Children,Comedy,Fantasy | Moana (2016) |
Adventure,Animation,Children,Comedy,Fantasy | Emperor’s New Groove, The (2000) |
أول خمسة أفلام كلها لديها نفس رموز النوع مثل فلم Toy Story و لذلك لديها تشابه جتا يساوي 1. بالإضافة للبيانات المستخدمة هناك ثلاثة عشر فلماً بتشابه يساوي 1. أكثر فلم متشابه دون تطابق في الرموز كان فلم The Ant Bully و الذي كان يمتلك رمزاً إضافي “IMAX”.
تصفية تعاونية بسيطة (Simple Collaborative Filtering)
التصفية التعاونية تقترح عناصر بناءاً على ما يثير إعجاب مستخدمين مشابهين. من حسن الحظ أن مجموعة بيانات MovieLens تحتوي على كم كبير من بيانات المستخدمين في صورة تقييمات كل مستخدم، حيث أن كل مستخدم يعطي كل فلم شاهده قيمة من 1 إلى 5 لتمثيل مدى اعجابه بكل فلم. يمكننا النظر لمشكلة اقتراح فلم معين للمستخدم كمهمة تنبؤ كالآتي: باستخدام تقييم المستخدم لأفلام أخرى، ماذا سيكون التقييم المحتمل لهذا الفلم؟
طريقة بسيطة لعمل ذلك هي تعيين تقييم موزون لكل عنصر باستخدام تقييمات المستخدمين الآخرين
حيث أن هي القيمة المتنبئة للعنصر للمستخدم . و مقياس لدرجة التشابه بين المستخدمين و ، والعنصر هي التقييم المعروف من مجموعة البيانات للعنصر للمستخدم .
لقياس التشابه بين المستخدمين سنستخدم تقييم الأفلام لكل مستخدم. سنعتبر المستخدمين ذوو التقييمات المتشابهة متشابهين. لنعمل ببيانات التقييم يجب تسويتها (normalize) أولاً. يمكننا تسوية البيانات في ثلاثة خطوات:
أولاً, سنطرح متوسط التقييمات الإجمالي (لكل الأفلام و المستخدمين) حتى تكون التقييمات المعدلة متمركزة حول الصفر. بعد ذلك سنكرر العملية لكل فلم لاحتساب معدل التقييمات لفلم معين. أخيراً سنطرح المتوسط لكل مستخدم لاحتساب الاختلافات بين المستخدمين (مثلاً قد يعطي أحد المستخدمين تقييمات أعلى من غيره).
رياضياً, سيكون التقييم المعدل
حيث أن هو التقييم الأساسي, هو المتوسط الإجمالي, هو المتوسط للفلم (بعد طرح المتوسط الإجمالي), و هو متوسط التقييم للمستخدم (بعد التعديل للمتوسط الإجمالي و متوسط التقييم للفلم). للتبسيط, سنرمز للتقييم المعدل ب كتفضيل المستخدم للفلم .
الآن لنحمل بيانات التقييمات و نحسب التقييمات المعدلة:
ratings = pd.read_csv("ratings.csv")
mean_rating = ratings['rating'].mean()
pref_matrix = ratings[['userId', 'movieId', 'rating']].pivot(index='userId', columns='movieId', values='rating')
pref_matrix = pref_matrix - mean_rating
item_mean_rating = pref_matrix.mean(axis=0)
pref_matrix = pref_matrix - item_mean_rating
user_mean_rating = pref_matrix.mean(axis=1)
pref_matrix = pref_matrix - user_mean_rating
الآن يمكننا تنبؤ تقدير مبدئي لتقييم مستخدم معين لفلم لم يشاهده بعد:
pref_matrix.fillna(0) + user_mean_rating + item_mean_rating + mean_rating
يمكننا حساب المسافة لمستخدم معين (في هذه الحالة المستخدم رقم 0) كالآتي:
mat = pref_matrix.values
k = 0
np.nansum((mat - mat[k,:])**2,axis=1).reshape(-1,1)
بذلك يتضح لنا أن اقرب مستخدم هو المتسخدم رقم 12 (المسافة بينهما تساوي صفر):
np.nansum((mat - mat[0,:])**2,axis=1)[1:].argmin()
np.nansum(mat[12] - mat[0])
و الآن نجد أن هناك فلمين شاهدها المستخدم رقم 12 لكن لم يشاهدها المستخدم رقم 0 بعد:
np.where(~np.isnan(mat[12]) & np.isnan(mat[0]) == True)
mat[12][[304, 596]]
للأسف المستخدم رقم 12 لم تعجبه الفلمين التي لم يشاهدها المستخدم رقم 0! يجب أن نكمل الحسابات لاحتساب جميع المستخدمين المقاربين.
نقاط ختامية
الطريقة المستخدمة في هذه التدوينة تعتمد على المجاورة بين البيانات (neighborhood-based) و قد رأينا نقطة ضعف محتملة لهذه الطريقة: الجيران قد لا يقترحون أي عناصر لم يشاهدها المستخدم المعين. كما أن هذه الطريقة لا تتوسع بطريقة جيدة عند زيادة عدد المستخدمين نظراً لحاجتنا لحساب المسافات بين كل نقطتين من البيانات.
في الجزء الثاني من هذه السلسلة سننظر لطريقة أخرى لبناء أنظمة اقتراحات باستخدام عوامل كامنة (latent factor). طريقة العوامل الكامنة تتفادى بعض نقاط الضعف في الطرق المبنية على المجاورة لكن لديها تحدياتها الخاصة بها.