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

الوحدة 6 — التوصية العميقة والتضمينات

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

البنية ذات البُرجَين

النموذج المرجعيّ في التوصية العميقة الحديثة (YouTube، Google) هو البُرجان: شبكة تُنتج تضمين المستخدم من ميّزات المستخدم، وشبكة أخرى تُنتج تضمين العنصر من ميّزات العنصر. القرار قربًا بينهما.

import torch
import torch.nn as nn

class TourUtilisateur(nn.Module):
def __init__(self, nb_users, dim_ctx, dim_out=64):
super().__init__()
self.emb_user = nn.Embedding(nb_users, 32)
self.reseau = nn.Sequential(
nn.Linear(32 + dim_ctx, 128), nn.ReLU(),
nn.Linear(128, 64), nn.ReLU(),
nn.Linear(64, dim_out),
)

def forward(self, user_ids, contexte):
e = self.emb_user(user_ids)
x = torch.cat([e, contexte], dim=-1)
return nn.functional.normalize(self.reseau(x), dim=-1)


class TourObjet(nn.Module):
def __init__(self, nb_items, dim_texte, dim_out=64):
super().__init__()
self.emb_item = nn.Embedding(nb_items, 32)
self.proj_texte = nn.Linear(dim_texte, 32)
self.reseau = nn.Sequential(
nn.Linear(64, 128), nn.ReLU(),
nn.Linear(128, 64), nn.ReLU(),
nn.Linear(64, dim_out),
)

def forward(self, item_ids, texte_emb):
e = self.emb_item(item_ids)
t = self.proj_texte(texte_emb)
return nn.functional.normalize(self.reseau(torch.cat([e, t], dim=-1)), dim=-1)


class ModeleDeuxTours(nn.Module):
def __init__(self, nb_users, nb_items, dim_ctx, dim_texte):
super().__init__()
self.user_tower = TourUtilisateur(nb_users, dim_ctx)
self.item_tower = TourObjet(nb_items, dim_texte)

def forward(self, users, ctx, items, texte):
u = self.user_tower(users, ctx)
i = self.item_tower(items, texte)
return (u * i).sum(dim=-1) # scalaire par paire

نُنهي كلّ بُرج بتعيير على كرة الوحدة (L2). النتيجة أنّ الضرب النقطيّ يصير تشابهًا كوسينوسيًّا، ممّا يُسهّل البحث لاحقًا.

المزيّة الأساس على تفكيك المصفوفة: بُرج المستخدم يستقبل ميّزات (سياق، ديموغرافيا، ساعة اليوم) لا مجرّد معرّف. مستخدم جديد ذو ميّزات معلومة يحصل على تضمين معقول من أوّل استفسار.

أخذ العيّنات السالبة

هنا تدخل المشكلة الحقيقيّة. البيانات المعلومة كلّها موجبة: من سجّل في الدورة تفاعل بها. لا نملك أمثلة سالبة صريحة. تدريب النموذج على إخراج قيم عالية لكلّ الأمثلة يُنتج نموذجًا يعطي درجة عالية لكلّ شيء.

الحلّ: نُولّد أمثلة سالبة اصطناعيًّا. لكلّ زوج موجب (u,i)(u, i)، نأخذ عنصرًا عشوائيًّا ii^- لم يتفاعل معه المستخدم، ونطلب من النموذج أن يُعطي درجة أعلى للزوج الموجب.

الخسارة الكلاسيكيّة هنا هي contrastive loss أو sampled softmax. الخيار المعياريّ في التوصية الصناعيّة اليوم هو In-batch negatives: كلّ زوج موجب في الدفعة يُعامَل بوصفه سالبًا لبقيّة المستخدمين في نفس الدفعة.

def perte_in_batch(u_emb, i_emb, temperature=0.1):
"""u_emb, i_emb: (B, dim_out) مع L2 معيّر."""
logits = u_emb @ i_emb.T / temperature # (B, B)
labels = torch.arange(u_emb.size(0), device=u_emb.device)
return nn.functional.cross_entropy(logits, labels)

كلّ سطر من logits صفٌّ فيه عنصر واحد موجب (على القطر) والآخر سالب. cross_entropy تُدفع نحو زيادة درجة الموجب وخفض درجات السوالب. هذا ما تفعله بشكل مباشر خسارة NT-Xent الشهيرة في التعلّم التمييزيّ.

تحيّز أخذ العيّنات: العناصر الشعبيّة تظهر كثيرًا في الدفعات فتُعامَل سالبةً مرارًا، ممّا يُعاقبها. التصحيح المشهور هو LogQ correction الذي يضيف مصطلحًا يُوازن هذا الأثر. غيابه يجعل النموذج ينحاز نحو العناصر النادرة، وهو أثر معاكس لما نريد أحيانًا.

الاستدعاء ثمّ الترتيب

على كاتالوغ حجمه مليون عنصر، لا يمكن حساب درجة النموذج لكلّ عنصر لكلّ استعلام. الحلّ المعياريّ هو تقسيم النظام إلى مرحلتَين.

مرحلة الاستدعاء (retrieval): تُنتج بضع مئات من العناصر المرشّحة بسرعة، عادةً بتشابه في فضاء التضمينات باستعمال بحث الجيران التقريبيّ. النموذج ذو البُرجَين مثاليّ لهذه المرحلة: نحسب تضمين المستخدم مرّة، ثمّ نبحث عن أقرب k في مؤشّر تضمينات العناصر المُعَدّ مسبقًا.

مرحلة الترتيب (ranking): تُطبّق نموذجًا أدقّ (وأثقل) على المرشّحين فقط. يمكن استعمال ميّزات لا يصلح استعمالها في المرحلة الأولى (ميّزات تقاطعيّة مثل «شاهد المستخدم دورة نفس المؤلّف»).

هذا التقسيم يقلّ التعقيد من O(N)O(N) لكلّ استعلام إلى O(logN)O(\log N) في الاستدعاء التقريبيّ، ثمّ O(K)O(K) في الترتيب الدقيق، حيث KK بضع مئات لا ملايين.

بحث الجيران التقريبيّ بـFAISS

مكتبة FAISS من Facebook هي المرجع في هذا الميدان. تبني مؤشّرًا يسمح بالبحث في ملايين المتجهات بميلي ثانية.

import faiss
import numpy as np

# item_vecs : (nb_items, 64) — مخارج بُرج العنصر بعد التدريب، معيّرة L2
item_vecs = torch.load("item_embeddings.pt").numpy().astype("float32")

# مؤشّر يعتمد HNSW: سريع، بلا حاجة إلى تدريب، ممتاز لعدد متوسّط
index = faiss.IndexHNSWFlat(item_vecs.shape[1], 32)
index.metric_type = faiss.METRIC_INNER_PRODUCT
index.add(item_vecs)

# استعلام مستخدم
user_vec = torch.load("un_user.pt").numpy().astype("float32").reshape(1, -1)
distances, indices = index.search(user_vec, 200) # top-200 candidats

IndexHNSWFlat يستعمل خرائط ناڤيغرور HNSW: شبكة يبنى فيها روابط بين المتجهات المتقاربة، فيصير البحث اجتيازًا سريعًا للشبكة بدل فحص كلّ العناصر. زمن الاستعلام حوالي ميلي ثانية على مليون عنصر بدقّة استرجاع تفوق 95%. هذه العناصر الـ200 تُمرَّر بعد ذلك إلى نموذج الترتيب.

متى نستحقّ العمق فعلًا؟

قاعدة صادقة: العمق يُثمر حين تكون واحدة من هذه الشروط قائمة.

  • ميّزات غنيّة: نصّ، صورة، سياق زمنيّ، جغرافيا. البُرجان يجمعانها في تضمين واحد.
  • مقياس هائل: أكثر من عشرة ملايين عنصر أو مليار تفاعل. الاستدعاء التقريبيّ يفتح هذا العالم.
  • إشارات ضمنيّة معقّدة: أكثر من ثنائيّة نقر / لم يُنقَر (كتخصيصات، ألعاب متكرّرة).

من دون هذه الشروط، تفكيك المصفوفة من الوحدة الثالثة ينافس نموذج البُرجَين مباشرةً، مع كسر تعقيد أقلّ بكثير. لا تُستنبَت شبكة عصبيّة على منصّة صغيرة إلّا بعد أن يستقرّ خطّها المرجعيّ التقليديّ.

الخلاصة

  • النموذج ذو البُرجَين يُنتج تضمينَين متكافئَين للمستخدم والعنصر، يجمعان الميّزات الغنيّة (نصّ، سياق) في فضاء واحد.
  • أخذ العيّنات السالبة ضروريّ لأنّ البيانات كلّها موجبة؛ الشكل السائد اليوم هو In-batch negatives مع خسارة سوفتماكس تمييزيّة.
  • بنية الاستدعاء ثمّ الترتيب تحلّ مشكلة القياس: بحث تقريبيّ سريع بـFAISS، ثمّ ترتيب دقيق على بضع مئات مرشّحة.
  • العمق يستحقّ الجهد حين تكون الميّزات غنيّة أو المقياس هائلًا؛ دون ذلك، تفكيك المصفوفة يبقى منافسًا حقيقيًّا.

الوحدة التالية: البداية الباردة، تلك الحالة التي يعجز فيها كلّ ما بنيناه حتّى الآن عن قول شيء موثوق.