محرك التوصيات
محرك التوصيات هو المحرك الصامت لاقتصاد الإنترنت: “شاهدت حلقتين، هاك مسلسل ستنهيه هذا الأسبوع” في Netflix، و”عملاء مثلك اشتروا أيضًا” في Amazon، والتشغيل التلقائي في YouTube. تحت الغطاء الأمر بسيط بشكل مفاجئ ، مصفوفة من المستخدمين بعناصر، أغلب خلاياها فارغة، والحيلة كلها أن تملأ الفجوات بشكل مقنع برياضيات اسمها التشابه. نفس الجبر الخطي الذي يدعم عمل pandas في المساق يتوسع ليشمل العائلتين الكبيرتين اللتين ستبنيهما هنا: الترشيح التعاوني (استمد الذوق من تقييمات المستخدمين الآخرين) والترشيح القائم على المحتوى (طابق عناصر جديدة مع ملفات تعريف الأشياء التي قيّمتها بالفعل). بحلول النهاية سيكون لديك نظام هجين يقدم توصيات منطقية فعلًا على مجموعة بيانات حقيقية من 100 ألف تقييم.
يفترض هذا Python 101 بالإضافة إلى معرفة عملية بـpandas ورياضيات مصفوفات NumPy ، وحدات تحليل البيانات في المساق. لا تعلم عميق، ولا أنظمة بمستوى صناعي. إنه اختياري وغير مصنّف؛ راجع المشاريع الواقعية للقائمة الكاملة.
🎯 ما ستفعله
- حمّل مجموعة بيانات تقييمات حقيقية في مصفوفة فائدة مستخدم-عنصر واستكشف فراغها (تخلخلها).
- احسب تشابه جيب التمام بين المستخدمين وبين العناصر بعمليات متجهات NumPy.
- توقع التقييمات المفقودة من متوسطات أقرب الجيران وقيّم دقتك بـMAE.
- ابنِ ملفات تعريف قائمة على المحتوى من أنواع النوع/سمات العنصر وولّد توصيات عناصر.
- ادمج درجات التعاوني والقائم على المحتوى في مُوصٍ هجين وتحقق منه.
أين تُشغّل هذا
محليًا مع uv هو المسار الأساسي: تُحمَّل مجموعة بيانات MovieLens كملفات CSV مسطحة يمكنك فحصها بـpandas، وخط الأنابيب كاملًا (امسح عداد أقرب الجيران، قارن الأخطاء، اطبع أسبابًا قابلة للشرح “لأنك أحببت”) تفاعلي في طرفية. uv add numpy pandas scikit-learn يغطي كل شيء.
GitHub Codespaces يمنحك التجربة نفسها: افتح codespaces.new/abderrahim-lectures/python-data-analysis-course ويُشغَّل كل أمر في تبويب متصفح مقابل نفس مجموعة البيانات.
Google Colab وKaggle Notebooks وBinder تشغّل خط أنابيب الحوسبة بأمانة ، مصفوفة التقييمات هي ~100 ألف تقييم حقيقي تناسب الذاكرة براحة، وتشابه جيب التمام جبر خطي، ومجموعة البيانات هي نفس ملف MovieLens العام الذي يستخدمه الطلاب دائمًا، لذا تطابق الأرقام في دفترك الأرقام في رأسك. لا مفاتيح API، لا GPU. الشيء الوحيد الذي لا يمكنك فعله في دفتر هو استيراد تخطيط ملفاتك الخاص ، ولحظة رغبتك في خدمة تقدم توصيات عبر HTTP، تكون تلك القمة محلية.
الإعداد
أحضر مجموعة الأدوات ومجموعة البيانات على القرص قبل أول ضرب متجهات نقطي.
ثبّت uv والاعتماديات
macOS / Linux (الطرفية):
curl -LsSf https://astral.sh/uv/install.sh | sh
Windows (PowerShell):
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
أغلق وأعد فتح طرفيتك، ثم:
uv --version
mkdir recommendation-engine && cd recommendation-engine
uv init --bare
uv add numpy pandas scikit-learn
حمّل مجموعة بيانات MovieLens 100k
mkdir -p data
curl -L -o data/ml-100k.zip https://files.grouplens.org/datasets/movielens/ml-100k.zip
unzip -o data/ml-100k.zip -d data
ls data/ml-100k/ | head -20
الملفات الثلاثة التي تحتاجها فعلًا: u.data (التقييمات: user item rating timestamp)، وu.item (بيانات تعريف الأفلام، مفصولة بـ|، الأنواع في آخر 19 عمودًا)، وu.user (user age ... occupancy). كل ما عدا ذلك توثيق.
✅ قائمة التحقق
- ✅ يطبع
uv --versionنسخة؛ والاعتمادياتnumpyوpandasوscikit-learnمثبتة عبرuv add. - ✅
data/ml-100k/u.dataموجود وhead -3يعرض صفوفuser item rating timestamp. - ✅
data/ml-100k/u.itemموجود (أنابيب)، وdata/ml-100k/u.userموجود (مفصول بـ|أيضًا).
الخطوة 1: حمّل التقييمات في مصفوفة مستخدم-عنصر
تعيش محركات التوصيات وتموت بطريقة تحوّل سجل الأحداث الخام إلى مصفوفة. مصفوفة مستخدم × عنصر بتقييمات في الخلايا ، وأغلبية ساحقة من الخلايا فارغة، لأن كل مستخدم يقيّم فقط القليل من ألف فيلم ، هي الشكل المعياري. تنتج هذه الخطوة ذلك الشكل وتقيس مدى فراغه (تخلخله).
👟 تلميح البداية : ابدأ بكتابة load_ratings(path) التي تقرأ u.data بـpd.read_csv(..., sep="\t", header=None) وأسماء الأعمدة الأربعة، ثم اطبع head() ، شاهد صفوف الأحداث الخام قبل إعادة تشكيلها في مصفوفة.
# engine.py
import numpy as np
import pandas as pd
RATINGS = "data/ml-100k/u.data"
ITEMS = "data/ml-100k/u.item"
def load_ratings(path: str) -> pd.DataFrame:
df = pd.read_csv(path, sep="\t", header=None,
names=["user", "item", "rating", "ts"])
return df
ratings = load_ratings(RATINGS)
print(ratings.head())
print(f"users={ratings['user'].nunique()} items={ratings['item'].nunique()} "
f"total={len(ratings)}")
matrix = ratings.pivot_table(index="user", columns="item", values="rating")
print("matrix shape:", matrix.shape)
print("sparsity :", f"{(1 - matrix.notna().sum().sum() / (matrix.shape[0] * matrix.shape[1])):.4%}")
pivot_table هو مصنع المصفوفات بسطر واحد: index=المستخدمون، columns=العناصر، values=التقييمات، وكل زوج غير مُقيَّم يسقط كـNaN ، وهو بالضبط ما نريده، لأن NaN هو مشكلة التوصيات: املأ الفجوات. سطر التخلخل هو فحص الواقع الهندسي: عند ~94–95% يجيب على “كم من المصفوفة نعرفه فعلًا؟” قبل أي توصية ، والجواب هو مبرر المجال كله المسمى الترشيح التعاوني (علينا أن نستدل من أصوات المستخدمين الآخرين).
🎯 الناتج المتوقع : خمسة صفوف تقييمات مفصولة بعلامات تبويب، وusers=943 items=1682 total=100000، ومصفوفة متخلخلة 943×1682، وتخلخل ~94-95%.
🩹 إذا لم يعمل : إذا فشل تحليل u.data، فالتنزيل لم يكتمل ، تحقق من حجم الملف (≈1.9 MB) وأعد تشغيل curl. إذا طُبع التخلخل كـ~0%، فقد ملأت pivot_table الفجوات بـ0 بدل NaN ، لا تمرر fill_value (الافتراضي يترك الفجوات NaN، بينما fill_value=0 الصريح يعلّم كل عنصر غير مُقيَّم كأنه “مكروه” بصمت، ما يفسد كل تشابه لاحق).
✅ قائمة التحقق
- ✅
matrix.shape == (943, 1682)بداخلها ثقوبNaN. - ✅ يمكنك طباعة عمود مستخدم واحد إجمالًا (
matrix.loc[1].nunique()) وهو ~20-30. - ✅ يمكنك ذكر سبب كون فراغٍ بنسبة 94% مثيرًا للاهتمام لا خطأً.
🤔 سؤال (أسئلة) سقراطي(ة)
- الكثافة ~6% تعني أن 94% من الشبكة مجهولة. إذا صوّت مستخدم على 30 فيلمًا، فقد تعني “التوصية” غالبًا “تخمينًا في أغلبه”. ما الافتراض الذي يجب أن يستمر الترشيح التعاوني في حمله عبر المستخدمين (عن الذوق المشترك) قبل أن تستحق تلك التخمينات ثقة؟
pivot_tableيمنحناNaNلغير المُقيَّم. لماذا خطر حقيقي أن تملأ مسبقًا بـ0؟ ماذا ستفعل بتشابه جيب التمام لمستخدم يصادف أنه يكره كل شيء جرّبه؟
الخطوة 2: احسب تشابه جيب التمام بين المستخدمين
عملة المحرك الأساسية هي التشابه ، رقم يقول كم تقترب أذواق مستخدمين اثنين. يقارن تشابه جيب التمام متجهي تقييم كاتجاهين: المستخدمون الذين يقيّمون بشكل مشابه (مقيس) يحصلون على جيب تمام مرتفع بغض النظر عما إذا كانوا يستخدمون سلم 0–5 كاملًا، لأن جيب التمام يتجاهل المقدار. تحويل متجهات NumPy يحوّل مقارنة صف × صف إلى ضربة بث واحدة فوق مصفوفة.
👟 تلميح البداية : ابدأ بكتابة cosine_similarity(a, b) التي تقنع NaN بـ~np.isnan قبل الضرب النقطي، وتحقق منها على متجهي تقييم متطابقين ، يجب أن يعيدا 1.0 ، قبل توجيهها إلى مصفوفة المستخدمين كاملة.
# engine.py (continued)
import numpy as np
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float:
a = a[~np.isnan(a)]
b = b[~np.isnan(b)]
# NaNs collapse — compare only the pair's common ratings
a0, b0 = a[: min(len(a), len(b))], b[: min(len(a), len(b))]
if a0.size == 0:
return 0.0
denom = np.linalg.norm(a0) * np.linalg.norm(b0)
return float(np.dot(a0, b0) / denom) if denom > 0 else 0.0
users = ratings["user"].unique()
test = matrix.loc[[users[0], users[1]]].to_numpy()
print("cos(user 1, user 2):", cosine_similarity(test[0], test[1]))
التفصيل الحرج هو القناع: ~np.isnan يُسقط الثقوب، لذا نقارن فقط الأفلام التي قيّمها المستخدمان فعلًا ، تقاطعًا، لا المتجه كاملًا. np.dot(a0, b0) / (|a0|·|b0|) هو جيب التمام النصّي؛ حارس 0.0 يلتقط حالة الأصفار الكلية المتدهورة حيث ينفجر المقام. المقارنة كلها ~4 أسطر من NumPy، وهذه حقيقة رياضيات المُوصِيات: الخوارزمية بسيطة، وصحة البيانات حيث يقع العمل الحقيقي.
🎯 الناتج المتوقع : رقم عائم نموذجيًا في [0, 0.3] للمستخدمين المقرونين عشوائيًا ، معظم درجات التشابه المريح تقع منخفضة، وهذا صحيح: يتشارك المستخدمون بضعة أنواع، لا كل ذوق بعضهم.
🩹 إذا لم يعمل : إذا حصلت على nan، فمستخدمان لم يتشاركا أي عنصر مُقيَّم والمصفوفتان المقنعتان بطول 0 ، قص min(...) ينهار كلاهما إلى 0 وكان يجب أن يعيد حارس size == 0 الرقم 0.0؛ إذا أزلت الحارس، فأعده. إذا التصقت الدرجات بـ1.0 للجميع، فالقناع معطوب وNaN تتسرب إلى الضرب النقطي.
✅ قائمة التحقق
- ✅ استدعاء اللعبة يطبع رقمًا عائمًا في
[0, 1]، ولأزواج المستخدمين العشوائية يكون صغيرًا. - ✅ متجهو تقييم متطابقان يعيدان
1.0(فحص في الكونسول:cosine_similarity(np.array([5.,5.,0.]), np.array([5.,5.,0.]))).
🤔 سؤال (أسئلة) سقراطي(ة)
- يتجاهل جيب التمام المقدار ، مستخدم يقيّم كل شيء 4–5 وآخر يقيّم 0–1 ما زالا ربما بجيب تمام قريب 1.0 إذا تطابقت ترتيباتهما. متى تكون تلك الودية صحيحة للتوصيات، ومتى يكون ارتباط بيرسون (التقييمات المركزية) الخيار الأسلم ، اذكر سيناريو ذوق أفلام واقعيًا؟
- إسقاط
NaNللمقارنة بالتقاطع فقط هو أقرب جيران لمجموعة فرعية من التقييمات المشتركة. إذا تشارك مستخدمان فيلمًا واحدًا، فجيب التمام على ذلك الزوج هو1.0(أي شيء يشبه نقطة واحدة). ما العتبة التي يجب أن تفرضها (أدنى عناصر مشتركة)، وأين تصادم “الأحياء الأكبر للإنقاذ”؟
الخطوة 3: التوقع التعاوني ، متوسط أقرب مستخدم k
التشابه وحده لا يوصي؛ التجميع يفعل. لمستخدم وفيلم لم يقيّمه بعد، يكون التوقع التعاوني: ابحث عن k مستخدمين الأكثر شبهاً به، وحوّل متوسط تقييماتهم (مرجحًا بالتشابه إن أردت التميز)، فذلك المتوسط هو التخمين. يعمل بسبب رهان “دائرة الثقة”: الأشخاص ذوو الذوق المتطابق فيما لدينا يتفقون على ما ليس لدينا.
👟 تلميح البداية : ابدأ بكتابة predict_rating(ratings, matrix, u, m, k): حلق على كل مستخدم آخر، واحسب cosine_similarity، واحتفظ بالمستخدمين الذين قيّموا الفيلم m واجتازوا بوابة sim > 0.1، ثم أعد المتوسط المرجح بالتشابه لتقييماتهم لـm.
# engine.py (continued)
def predict_rating(root: pd.DataFrame, matrix: pd.DataFrame, u: int, m: int, k: int = 10) -> float:
target = matrix.loc[u]
scores = {}
for v in matrix.index:
if v == u:
continue
sim = cosine_similarity(target.to_numpy(), matrix.loc[v].to_numpy())
if pd.notna(matrix.loc[v, m]) and sim > 0.1:
scores[v] = sim
neighbors = sorted(scores, key=scores.get, reverse=True)[:k]
if not neighbors:
return float("nan")
numer = sum(scores[v] * matrix.loc[v, m] for v in neighbors)
return numer / sum(scores[v] for v in neighbors)
movie = 50
for u in [1, 42, 200]:
print(f"user {u} predict movie {movie}: "
f"{predict_rating(ratings, matrix, u, movie, k=10):.2f}")
الحلقة قوة غاشمة (كل مستخدم آخر، كل استدعاء) ، بطيئة بشكل مرعب عن قصد؛ يستخدم الإنتاج رياضيات مصفوفة كاملة متجهة وبحث O(1). الصغير الصحيح يتغلب على السريع المعقّد هنا. بوابة sim > 0.1 زائد جيران k هما زوجا الضبط (أي قدر من “التشابه” يعد تشابهًا، وحجم الدائرة). المتوسط المرجح sum(sim·rating)/sum(sim) هو مُنبئ بثلاثة أسطر بالكاد حملته أنظمة العالم الحقيقي.
🎯 الناتج المتوقع : أرقام عائمة معقولة حول 3–4 للمستخدمين الثلاثة ، العينة الصغيرة مصممة لـ”أرقام منطقية”، لا لدقة إنتاجية؛ فرق تقييم واحد بمقدار ±0.5 يظهر فعلًا في منزلة عشرية.
🩹 إذا لم يعمل : إذا طبع nan، فلم يجتز أي جار بوابة sim > 0.1 ، الفيلم أو المستخدم متخلخل جدًا؛ اخفض البوابة إلى 0.05 أو انزل إلى k=5. إذا كان كل توقع ~4.5 (تباين صغير)، فالمستخدم الأقرب يهيمن؛ صغّر k إلى 3 وشاهد التباين يعود. إذا استغرق التوقع ثلاث مرات 40 ثانية، فهذه التكلفة المتوقعة للقوة الغاشمة ، طوّر درس “وقت الحائط = التعقيد”، ولا تحسّنه بعيدًا بعد.
✅ قائمة التحقق
- ✅ ثلاثة توقعات تُطبع، كلها في
[1, 5]، وnanفقط حين لا يوجد جار مؤهل. - ✅ مستخدم قيّم الفيلم الهدف 5، وتوقع عبر الجيران، يقع قرب 4-5: تعيد دائرة الثقة إنتاج الذوق.
- ✅ يمكنك شرح دور كلٍّ من
k(الشجاعة) والبوابة (النقاء) في جملة واحدة.
🤔 سؤال (أسئلة) سقراطي(ة)
- التوقع متوسط مرجح حيث الأوزان تشابهات. إذا استبدلت متوسطًا غير مرجح (
1/k)، ماذا يحدث لمستخدم جاره الوحيد الشبيه مخطئ تمامًا لهذا الفيلم بالذات؟ كيف يتدهور الترجيح بأناقة (ومتى لا يتدهور ، فكّر في “جار واحد عالي التشابه بتقييم واحد”)؟ - حالة
nanإشراف حقيقي على ركن متخلخل. لبدء بارد لمستخدم جديد (لا تقييمات)، كل فيلم يعيدnanمن هذه الطريقة. هذا جدار من الطوب يصطدم به محركك لحظة لقائه مستخدمًا جديدًا كليًا ، وهي بالضبط لماذا توجد الخطوة 4 (القائمة على المحتوى). وضح كيف يغطي الهجين الفجوة التي لا يراها التعاوني.
الخطوة 4: الترشيح القائم على المحتوى من سمات العنصر
يموت الترشيح التعاوني عند البدء البارد ، فيلم جديد (لا تقييمات بعد)، مستخدم جديد (لا تاريخ). يتجاهل القائم على المحتوى المستخدمين الآخرين تمامًا: يصف العناصر بسماتها الخاصة (الأنواع، الوسوم، الكلمات المفتاحية) ويتنبأ “إذا أعجبك عنصر أ، ستُعجبك عناصر أخرى يشبه ملف تعريف سماتها ملف تعريف أ”. يبادل المحرك الحشد ببصمة العنصر الخاصة ، وفجأة تصبح الأفلام والمستخدمون الجدد قابلين للتوصية لحظة وجودهم.
👟 تلميح البداية : ابدأ بكتابة load_items(path) التي تقرأ u.item بـsep="|" وتحتفظ بـitem وtitle وأعمدة الأنواع، ثم ابنِ content_profile(items, rated) كمجموع مرجح بالتقييم لصفوف الأنواع للعناصر المقيَّمة.
# engine.py (continued)
ITEMS_COLS = ["item", "title", "date", "video", "url"] + [f"g{i}" for i in range(19)]
GENRES = ["Action", "Adventure", "Animation", "Children's", "Comedy", "Crime",
"Documentary", "Drama", "Fantasy", "Film-Noir", "Horror", "Musical",
"Mystery", "Romance", "Sci-Fi", "Thriller", "War", "Western"]
def load_items(path: str) -> pd.DataFrame:
df = pd.read_csv(path, sep="|", header=None, names=ITEMS_COLS,
encoding="latin-1")
df = df[["item", "title"] + GENRES].copy()
for g in GENRES:
df[g] = df[g].fillna(0)
return df
items = load_items(ITEMS)
print(items.shape, items.head(2).loc[:, ["item", "title"]].to_dict("records"))
def content_profile(items: pd.DataFrame, rated: dict) -> np.ndarray:
profile = np.zeros(len(GENRES))
for item, r in rated.items():
row = items.loc[items["item"] == item]
if row.empty:
continue
profile += r * row.iloc[0][GENRES].to_numpy()
return profile
profile = content_profile(items, {m: v for m, v in {
1: 5, 50: 3, 100: 4}.items()})
print("genre profile:", dict(zip(GENRES, profile.round(2))))
التحول يختزل إلى متجهات سمات: كل فيلم متجه ثنائي فوق الأنواع (1 إذا كان دراميًا، وإلا 0)، و”ملف تعريف مستخدم” هو المجموع المرجح بالتقييم لأنواع كل ما أحبوه ، Documentary: 5.0 بجانب Horror: 0.0 يقول “شاهد هذا المستخدم وثائقيًا وقيّمه 5”. بعد ذلك، مطابقة فيلم-ملف-تعريف ليست إلا تشابه جيب التمام مجددًا ، نفس الرياضيات في الخطوة 2، مطبقة على سمات العنصر بدل تقييمات المستخدم. يتولى encoding="latin-1" بايتات عناوين أواخر التسعينيات؛ وfillna(0) يبتلع السلاسل الفارغة التي تخفيها بعض الخلايا.
🎯 الناتج المتوقع : إطار عناصر 1682×20 (item، title، 18 علامة نوع) وملف تعريف أنواع للتقييمات التجريبية ، مثل {'Documentary': 5.0, 'Drama': 4.0, ...} حيث تسيطر الأنواع التي أطعمتها تقييمات عالية.
🩹 إذا لم يعمل : إذا لم يكن في items أعمدة أنواع، فلا يطابق GENRES حقول الأنبوب الـ19 الختامية في u.item ، عُدّ الأعمدة في سطر خام؛ ملف u.item يستخدم |، لذا sep="|" إلزامي. إذا كان ملف التعريف أصفارًا كلها، فـrow.empty ضُرب لكل عنصر ، معرّفات item في قاموس rated لديك غير موجودة في u.item؛ اطبع items["item"].min()/max() وطابق المعرّفات.
✅ قائمة التحقق
- ✅
items.shape == (1682, 20)وأعمدة الأنواع أعداد عائمة 0/1. - ✅ ملف التعريف متجه بطول 19 تسيطر فيه أنواع العناصر المقيَّمة.
- ✅ يمكنك ترتيب الأفلام لملف التعريف التجريبي بالجيب وتفوز بمطابقات الأنواع (ملف تعريف درامي عالٍ → دراما أولًا).
🤔 سؤال (أسئلة) سقراطي(ة)
- ملف التعريف متوسط مرجح لعلامات الأنواع ، والأنواع لغة خشنة (فيلم درامي ورومانسي معًا). حين تجمع المتجهات، يرى المستخدم الذي يحب النصف الرومانسي فقط من الدرامي الرومانسي وزن الدراما أيضًا. سمِّ الالتواء الواقعي الذي يخلطه ذلك بالذوق، وسمة ثانية وراء الأنواع تقلل ذلك الضجيج (مخرج؟ ممثلون؟ كلمات مفتاحية؟ عقد الإصدار؟).
- يدور كل شيء في القائم على المحتوى حول مشفرات العنصر الذاتية، لذا تكون توصية الفيلم قابلة للشرح: “أحببت الدراما والوثائقي”. أعلن الفشل لحظة كون القائمَ على المحتوى وحده هو الجواب في منصة حيث ملايين يقيّمون كل شيء ، ما البقعة العمياء التي تجعل التعاوني لا غنى عنه؟
الخطوة 5: المزج الهجين ، اجمع الإشارتين
ألقِ محرك توصيات في قاعدة كود حقيقية وسيصبح السؤال ليس “تعاونيًا أم قائمًا على المحتوى؟” ، بل “كيف نمزج الاثنين، ومتى يفوز كل منهما؟” الهجين امتزاج: اختر الجيران للتوقع التعاوني، وابنِ ملف تعريف محتوى من تاريخ المستخدم، واجمعهما في قائمة مرتبة واحدة بوزن α (0 = محتوى فقط، 1 = تعاون فقط). مقبض ألفا هو قصة الضبط كلها ، محور صغير، قفزة كبيرة.
👟 تلميح البداية : ابدأ بكتابة recommend(items, matrix, u, k, alpha, n): جمّع عناصر المستخدم المقيَّمة، وابنِ ملف تعريف محتوى، ثم سجّل كل فيلم غير مُقيَّم كـalpha * collab + (1 - alpha) * content ورتّب أعلى n.
# engine.py (continued)
def recommend(items: pd.DataFrame, matrix: pd.DataFrame, u: int, k: int = 10,
alpha: float = 0.5, n: int = 5) -> list[tuple]:
rated = {m: matrix.loc[u, m] for m in matrix.columns if pd.notna(matrix.loc[u, m])}
profile = content_profile(items, rated)
scores = {}
for m in matrix.columns:
if m in rated:
continue # don't recommend what's already seen
collab = predict_rating(ratings, matrix, u, m, k=k)
content = cosine_similarity(profile, items.loc[items["item"] == m, GENRES].to_numpy()[0]) if not items.loc[items["item"] == m].empty else 0.0
scores[m] = (alpha * collab if pd.notna(collab) else 0) + (1 - alpha) * content
ranked = sorted(scores, key=scores.get, reverse=True)[:n]
return [(items.loc[items["item"] == m, "title"].iloc[0], round(scores[m], 3)) for m in ranked]
for alpha in [0.0, 1.0]:
print(f"alpha={alpha}")
for title, s in recommend(items, matrix, 1, k=10, alpha=alpha):
print(" ", title, s)
سر الامتزاج أن alpha يشكل نفس القائمة المرتبة ، 0.0 يرتب خالصًا حسب ذوق الأنواع المرصود للمستخدم بينما 1.0 يرتب خالصًا حسب أصوات الجوار، والنقطة الحلوة تُقحم ملفات المخاطرة: على المستخدمين المتخلخلين، ينقذ المحتوى الذيل؛ وعلى الكثيفين، يفوز التعاوني بالرأس. تشغيلان، نفس المستخدم، وتشاهد الخمسة الأوائل تُخلط ، تلك حجة “لماذا الهجين” كلها تُقاس على الشاشة. يحمي alpha * collab قيمة nan التعاونية المفقودة بتصفيرها، فلا يسحب عنصر بارد توصيةً إلى الصفر بالمصادفة أبدًا.
🎯 الناتج المتوقع : لـalpha=0.0 قائمة مدفوعة بالأنواع (أنواع المستخدم 1 المفضلة ظاهرة في العناوين)؛ لـalpha=1.0 قائمة مدفوعة بالجيران تختلف مرئيًّا؛ درجات سليمة في [0, 1] بعد المجموع المرجح.
🩹 إذا لم يعمل : إذا طبعت إحدى قيم ألفا قائمتين متطابقتين، فإن predict_rating يعيد nan لكل عنصر وscores قائمة على المحتوى فعلًا ، ارفع البوابة أو صغّر k؛ لا يجب أن يهيمن collab صفريًا. إذا تسلقت الدرجات فوق 1، فقد أضاف المجموع المرجح بألفا عدم تطابق توزيع (جيب [0,1] مقابل متوسط جار [0,5]) ، طبّع ذراع التعاوني (/5) حتى يقحم ألفا تفاحًا بتفاح. إذا استغرق مستخدم واحد دقائق، فقوة غاشمة predict_rating لكل عنصر تتراكم ، ذلك متوقع؛ حوّل المتجهات لاحقًا، أو صغّر k وعدد أعمدة المرشحين للحفاظ على العرض حيًّا.
✅ قائمة التحقق
- ✅ تشغيلا ألفا يولّدان قائمتي أفضل خمس مختلفتين مرئيًّا للمستخدم نفسه.
- ✅ عناصر البدء البارد (لا جار مُقيَّم) ما زالت تُرتب عبر ذراع المحتوى عند
alpha < 1. - ✅ تبقى الدرجات في نطاق قابل للمقارنة، ويمكنك ذكر وقت فوز كل ذراع.
🤔 سؤال (أسئلة) سقراطي(ة)
- عند
alpha=0القائمة محتوى خالص؛ وعندalpha=1تعاون خالص. صف تجربة قابلة للقياس (مجموعة محتفظ بها، MAE على التقييمات المحتفظ بها) تخبرك أي ألفا يفوز لمجموعة بياناتك ، وفخ ضبط ألفا على نفس البيانات التي تقرّ بها. - يصل مستخدمون جدد بحفنة نقرات يسيرة؛ يجب أن يوصي المحرك من لا شيء تقريبًا. يترك الامتزاج المحتوى يحمل أول دستة توصيات. ما السبب الأعمق أن تتعاونًا خالصًا يزداد سوءًا قبل أن يتحسن مع نمو قاعدة مستخدميك من 50 إلى 5000 ، ولماذا يتقدم “متوسط الجيران” بشكل سيئ في أشد الأنظمة كثافة؟
⚠️ مآزق شائعة
- شبكة
NaNمملوءة تسمّم كل شيء بصمت. الملء المسبق للخلايا غير المقيَّمة بـ0يعلّمها “مكروهة”، ما يجر تشابه جيب التمام نحو تشابه-بعدم-المشاهدة وينفخ كل ضرب نقطي بالأصفار. أبقِ ثقوبNaN؛ وقنّعها (~np.isnan) عند كل مقارنة. - مقارنة أصفار خام من سلم غير مطبّع. مستخدمان بذوق متطابق، أحدهما يقيّم كل شيء 4-5 والآخر 0-1، يظهران كتشابه منخفض رغم تطابق الترتيبات. مركِّز التقييمات (اطرح متوسط كل مستخدم) قبل التشابه ، أي بيرسون ، حين يهم انضباط السلم.
- تقييم مشترك واحد ⇒ تشابه 1.0. أي مستخدمين بفيلم مشترك واحد “مطابقان” بحسب جيب التمام. بوابة على حجم تقاطع أدنى (مثلًا، 3 تقييمات مشتركة) لوقف الأشباه المتحللة من الهيمنة على الجوار.
- بدء بارد دون مخرج محتوى. فيلم جديد كليًا (لا تقييمات) لا يمكن التنبؤ به تعاونيًّا، ومستخدم جديد لا يستطيع تشكيل جوار. وكلاهما بالضبط ما وُجدت ذراع المحتوى لتغطيته ، هجين لا يدمج السمات هجين اسمًا فقط.
- ضبط ألفا على التقرير نفسه. اختيار
αبمدّ النظر إلى “ما يبدو جميلًا” على مجموعة التدريب يفرط في ملاءمة العرض. احتفظ بشريحة تقييمات، واختر ألفا الذي يقلل MAE على تلك الشريحة المحتفظ بها، وأبلغ عن ذلك الرقم ، الانضباط الذي ستحتاجه فعلًا في الإنتاج.
ما بنيته للتو
محرك توصيات حقيقي: حمّلت مجموعة بيانات MovieLens 100k في مصفوفة مستخدم-عنصر 943×1682، وقست تخلخلها البالغ 94%، وحسبت تشابه جيب التمام بين المستخدمين في NumPy، وتوقعت التقييمات المحتفظ بها بمتوسط مرجح لأقرب مستخدم k، وبنيت ذراع محتوى بملف تعريف أنواع من سمات عنصر، ومزجت الاثنين في قائمة مرتبة قابلة للضبط. عائلتان من رياضيات التوصيات تشغّلان أنظمة الإنتاج، مجموعة بيانات واحدة، ~150 سطرًا كودًا مرئيًّا. الأجزاء القابلة للنقل تتجاوز الأفلام بكثير: عادة التشابه المقنّع، وانضباط “امزج، واضبط على احتفاظ، وأبلغ”، ولحظة الإحساس بحجة التخلخل كرقم لا كاستعارة.
شغّل نسخة أكمل دون أي إعداد محلي
examples/recommendation-engine/ في مستودع المساق يجمع المحرك الكامل ومحمّل بيانات MovieLens ودفترًا يحمّل ويعكس ويسجّل ويضبط الهجين inline. استنسخ المستودع، أو افتحه في GitHub Codespaces، وشغّل الخطوات الخمس من البداية إلى النهاية.
إلى أين تذهب من هنا
- أضف التقييم بالشكل الصحيح: قسّم التقييمات 80/20 إلى تدريب/اختبار، وقس MAE على الشريحة المحتفظ بها لكلتا الذراعين (ولكل ألفا) من إعداد YAML، واطبع الفائز. هذه الإضافة الوحيدة التي تحوّل عرضًا إلى محرك قابل للدفاع.
- حوّل الجوار إلى متجهات: استبدل حلقة
for-فوق-المستخدمين القوةَ الغاشمة باستدعاء تشابه مصفوفة كاملة (طبع أولًا) ، ستشاهد حلقة دقائق-لكل-مستخدم تسقط إلى ميلي ثانية وتتذوق مكسب هندسة عادة NumPy. - قدّم API: غلّف
recommendفي نقطة نهايةFastAPI(/recommend/{user_id}?alpha=0.6) بجدول عناصر متوافق مع الاستعلام ، الدالة نفسها، يمكن الوصول إليها الآن عبر HTTP، زائد شارة يمكنك فتحها في متصفح. - جرّب مجموعة البيانات الأخرى: استبدل
u.dataبتقسيماتu1.base/u1.testالرسمية المشحونة في نفس تنزيلml-100k، وأبلغ عن MAE مجموعة الاختبار حين يُضبط ألفا على شريحة التدريب. فجوة الأرقام نظرة صادقة على التعميم.
شارك مشروعك مع الصف
لديك مُوصٍ يتغلب على العشوائية، أو امتزاج هجين تفتخر به، أو محرك مُقيَّم بمعيار MAE تستطيع اقتباسه؟ examples/student-projects/ معرض لمشاريع قدّمها طلاب آخرون، ويأخذك README الخاص به في جولة إضافة مشروعك عبر طلب سحب من البداية إلى النهاية: الشوكة، والفرع، والالتزام، وفتح الـPR. لا خبرة git مسبقة مفترضة.
مرحبًا بك في كتابة Python خارج المتصفح. 🎓
أكمل كل خطوة ثم حدد المشروع كمكتمل لجمع نقاطه.