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

الوحدة 3 — تفكيك المصفوفات وSVD

تصفية الجوار جوابها حسابيّ: نقارن كلّ اثنين. تفكيك المصفوفات جوابه بنيويّ: نُفسّر المصفوفة كلّها بعوامل قليلة خفيّة. هذه القفزة المفهوميّة رفعت جودة التوصية بشكل ملحوظ في جائزة Netflix، وهي الأساس النظريّ لكلّ ما سيأتي في النماذج العميقة (الوحدة 6).

الفكرة: عوامل كامنة

نفترض أنّ سلوك التقييم يُلخّصه عدد قليل من الأبعاد المخفيّة. لنقل k=10k=10 عوامل. لكلّ مستخدم متجه puR10p_u \in \mathbb{R}^{10} يقول: كم يُقدّر هذا المستخدم كلّ بُعد (الحدّة، الصعوبة، الطابع العمليّ، إلخ. من دون أن نُسمّي هذه الأبعاد أو نعرفها مسبقًا). ولكلّ عنصر متجه qiR10q_i \in \mathbb{R}^{10} يقول: كم يُعبّر هذا العنصر عن كلّ بُعد.

يُقدَّر تقييم uu للعنصر ii بالضرب النقطيّ:

r^u,i=puqi=k=1Kpu,kqi,k\hat{r}_{u,i} = p_u^\top q_i = \sum_{k=1}^{K} p_{u,k}\,q_{i,k}

نُطلق على المتجهَين اسم العوامل الكامنة لأنّها لا تُلاحَظ في البيانات؛ ينبثقان من الأمثل. النموذج يُحوّل كلّ مصفوفة RR حجمها m×nm \times n إلى ضرب مصفوفتَي PP (m×Km \times K) وQQ (n×Kn \times K):

RPQR \approx P Q^\top

KK عدد صغير عادةً (10 إلى 200)، وهذا التخفيض للأبعاد هو ما يُعمّم إلى الخانات الفارغة.

لماذا لا يكفي SVD الرياضيّ

في الجبر الخطّيّ، لكلّ مصفوفة تفكيك R=UΣVR = U \Sigma V^\top (Singular Value Decomposition). يبدو الحلّ جاهزًا: نبتر بعد أوّل KK قيم مفردة ونحصل على أفضل تقريب رتبة KK بمعيار Frobenius. لكنّ هذا لا يعمل هنا لسببَين متكاملَين.

الأوّل: SVD الكلاسيكيّ يفترض معرفة كلّ الخانات. نحن نجهل 95% منها. سدّها بأصفار يقول للنموذج «كلّ ما لم يُسجَّل يستحقّ صفرًا»، وهذا مضاد للحقيقة.

الثاني: لا يوجد تنظيم، فأيّ ضجيج في التقييمات القليلة المعروفة يُعامَل كإشارة.

الحلّ الذي غيّر الميدان: بدل التفكيك المغلق، نبحث عن PP وQQ اللتَين تُدنيان الخطأ على الخانات المعروفة فقط، مع تنظيم. الطريقة اقترحها Simon Funk في مدوّنة عام 2006 خلال مسابقة Netflix، وأصبحت الأساس المعياريّ.

دالّة الخسارة وFunkSVD

الدالّة التي نُدنيها:

L=(u,i)K(ru,ipuqi)2+λ(pu2+qi2)L = \sum_{(u,i) \in \mathcal{K}} (r_{u,i} - p_u^\top q_i)^2 + \lambda\,(\lVert p_u \rVert^2 + \lVert q_i \rVert^2)

الجزء الأوّل خطأ تربيعيّ على الخانات المعروفة K\mathcal{K} فقط. الجزء الثاني تنظيم L2 يمنع القيم من الانفجار.

اختيار λ\lambda حاسم. قيمة قليلة جدًّا: يتحفّظ النموذج على تقييمات التدريب لكنّه يفشل في التعميم. قيمة كبيرة جدًّا: كلّ العوامل تتّجه نحو الصفر، والتنبّؤات كلّها تُصبح المتوسّط. النطاق العمليّ عادةً بين 0.01 و0.1، ويُختار بالتحقّق المتقاطع على قسم من التقييمات.

يُدرَّب النموذج بـالنزول التدرّجيّ العشوائيّ: نمرّ على التقييمات واحدًا واحدًا، ونحدّث pup_u وqiq_i باتّجاه خفض الخطأ المحلّيّ:

pupu+η(eu,iqiλpu)p_u \leftarrow p_u + \eta\,(e_{u,i}\,q_i - \lambda\,p_u) qiqi+η(eu,ipuλqi)q_i \leftarrow q_i + \eta\,(e_{u,i}\,p_u - \lambda\,q_i)

مع eu,i=ru,ipuqie_{u,i} = r_{u,i} - p_u^\top q_i. تُنجَز عدّة تمرّرات (10 إلى 30 حِقبة عادةً)، ومعدّل التعلّم η\eta حوالي 0.005.

الانحيازات: التصحيح الذي يُغيّر النتيجة

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

r^u,i=μ+bu+bi+puqi\hat{r}_{u,i} = \mu + b_u + b_i + p_u^\top q_i

μ\mu متوسّط كلّ التقييمات، bub_u انحياز المستخدم (متفائل أو متشائم)، bib_i انحياز العنصر (شعبيّ أو غير شعبيّ). في تجارب Netflix، إضافة هذه الانحيازات وحدها خفّضت RMSE بأكثر من 5%، وهذا فرق ضخم على مقياس ضيّق. تُدرَّب هي الأخرى بنزول التدرّج، وتُنظَّم بـL2.

Alternating Least Squares (ALS)

بديل SGD طريقة الحدّ الأدنى المتناوب. نُثبّت QQ ونحلّ لـPP بتحسين تربيعيّ (كلّ سطر مستقلّ)، ثمّ نُثبّت PP ونحلّ لـQQ، ونكرّر.

المزيّة: التوازي المحض. كلّ سطر من PP مستقلّ عن الآخر، فيوزَّع الحساب على معالجات كثيرة. Spark MLlib تستعمل ALS بالضبط لهذا السبب على مليارات التفاعلات. العيب: خطوة أثقل من خطوة SGD، فيصعب استعمالها في التدفّق المستمرّ.

قاعدة عمليّة: SGD (وFunkSVD) للمصفوفات المتوسّطة (حتّى بضعة ملايين تقييم)، وALS لما فوق ذلك، خاصّة على البنى الموزّعة.

التنفيذ بمكتبة surprise

import pandas as pd
from surprise import Dataset, Reader, SVD
from surprise.model_selection import GridSearchCV

df = pd.read_csv("ratings.csv")
reader = Reader(rating_scale=(1, 5))
data = Dataset.load_from_df(df[["userId", "courseId", "rating"]], reader)

# البحث الشبكيّ عن أفضل معلمات
grille = {
"n_factors": [20, 50, 100], # K عدد العوامل
"n_epochs": [20, 30], # عدد الحقب
"lr_all": [0.005, 0.01], # معدّل التعلّم
"reg_all": [0.02, 0.05, 0.1], # التنظيم L2
}

gs = GridSearchCV(SVD, grille, measures=["rmse"], cv=3, n_jobs=-1)
gs.fit(data)

print(f"أفضل RMSE: {gs.best_score['rmse']:.4f}")
print(f"أفضل معلمات: {gs.best_params['rmse']}")

# التدريب النهائيّ بأفضل معلمات
algo = gs.best_estimator["rmse"]
trainset = data.build_full_trainset()
algo.fit(trainset)

# التنبّؤ بتقييم مستخدم لدورة
uid, iid = "user_42", "course_docker_101"
pred = algo.predict(uid, iid)
print(f"التنبّؤ: {pred.est:.2f}")

الصنف SVD في surprise ليس SVD الكلاسيكيّ رياضيًّا، بل FunkSVD مع انحيازات. الاسم مؤسف تاريخيًّا. n_factors هو KK، وreg_all هو λ\lambda المشترك بين المتجهات والانحيازات.

قيود المُدرَّج على النقاط

كلّ ما سبق يفترض تقييمات صريحة على مُدرَّج (نجوم). التغذية الراجعة الضمنيّة (نقر / لم يُنقَر) تحتاج بديلًا يُعالج غياب الإشارة السلبيّة الصريحة، وهو iALS (implicit ALS) الذي يُوزَع فيه ثقلٌ لكلّ تفاعل موجب وثقلٌ صغير لكلّ خانة غير مرئيّة. سنعود إليه في الوحدة التاسعة.

ندرة الخانات المعروفة

لا تسدّ الخانات الفارغة بأصفار قبل تشغيل تفكيك المصفوفة. هذه غلطة كلاسيكيّة تُبدّل «لم يُقيَّم» بـ«قُيّم صفرًا»، فينحاز كلّ النموذج نحو التنبّؤ بأصفار. الخوارزميّات المخصّصة للتوصية (surprise، Spark MLlib) تعرف كيف تتعامل مع الغياب.

الخلاصة

  • تفكيك المصفوفة يُمثّل كلّ مستخدم وعنصر بمتجه من عوامل كامنة يُنبثق من الأمثل، لا من ميّزات معروفة.
  • SVD الكلاسيكيّ لا يعمل مباشرةً على المصفوفات النادرة؛ يُستبدَل بـFunkSVD: نزول تدرّجيّ على الخانات المعروفة مع تنظيم L2.
  • الانحيازات bub_u وbib_i تُصفّي المستوى الشخصيّ والشعبيّة، وتُنقص خطأ التنبّؤ نقصانًا كبيرًا وحدها.
  • ALS بديل موازٍ لـSGD؛ يُختار بحسب الحجم وطبيعة التدفّق.

الوحدة التالية: التوصية بالمحتوى، حين تنعدم التقييمات فلا نستطيع الاعتماد على التصفية التعاونيّة.