Post

Python में शुरुआत से रिकमेंडेशन सिस्टम बनाना

सीखें कि Python में शुरुआत से रिकमेंडेशन सिस्टम कैसे बनाएं। इस गाइड में कोलैबोरेटिव फ़िल्टरिंग, कंटेंट-आधारित फ़िल्टरिंग, मैट्रिक्स फ़ैक्टराइज़ेशन, Surprise लाइब्रेरी, scikit-learn इम्प्लीमेंटेशन, हाइब्रिड दृष्टिकोण और एक पूर्ण मूवी रिकमेंडेशन उदाहरण शामिल है।

Python में शुरुआत से रिकमेंडेशन सिस्टम बनाना

रिकमेंडेशन सिस्टम क्या हैं?

रिकमेंडेशन सिस्टम पिछले व्यवहार, आइटम विशेषताओं, या समान उपयोगकर्ताओं ने जो पसंद किया है, उसके आधार पर यह अनुमान लगाते हैं कि कोई उपयोगकर्ता क्या पसंद कर सकता है। वे अक्सर सेंटीमेंट विश्लेषण के साथ मिलकर काम करते हैं ताकि रिकमेंडेशन में उपयोगकर्ता की राय के संकेतों को शामिल किया जा सके। Netflix फ़िल्में सुझाता है। Amazon उत्पाद सुझाता है। Spotify प्लेलिस्ट सुझाता है। इन सबके पीछे रिकमेंडेशन एल्गोरिदम होते हैं।

तीन मुख्य दृष्टिकोण हैं:

  • कोलैबोरेटिव फ़िल्टरिंग — ऐसे आइटम सुझाएं जो समान उपयोगकर्ताओं ने पसंद किए। इसके लिए आइटम के बारे में कुछ भी जानने की आवश्यकता नहीं होती।
  • कंटेंट-आधारित फ़िल्टरिंग — आइटम विशेषताओं के आधार पर ऐसे आइटम सुझाएं जो उपयोगकर्ता ने पहले पसंद किए हैं उनके समान हों।
  • हाइब्रिड दृष्टिकोण — बेहतर परिणामों के लिए दोनों विधियों को संयोजित करें।

यह गाइड प्रत्येक दृष्टिकोण को Python में शुरुआत से लागू करती है, फिर दिखाती है कि प्रोडक्शन-क्वालिटी सिस्टम बनाने के लिए Surprise और scikit-learn जैसी लाइब्रेरीज़ का उपयोग कैसे करें।

जब मैंने Codiste में AI एजेंट फ्रेमवर्क बनाए जिनमें रिकमेंडेशन लॉजिक शामिल था, तो मैंने सीखा कि कोलैबोरेटिव और कंटेंट-आधारित फ़िल्टरिंग के बीच चुनाव अक्सर इस बात पर निर्भर करता है कि कोल्ड स्टार्ट पर आपके पास वास्तव में कौन-सा डेटा उपलब्ध है। प्रोडक्शन में, हम लगभग हमेशा हाइब्रिड दृष्टिकोण पर पहुंचते थे क्योंकि कोई भी विधि अकेले उपयोगकर्ता के सभी परिदृश्यों को संभाल नहीं पाती थी।

सेटअप करना

1
pip install numpy pandas scikit-learn scikit-surprise scipy

हम इस पूरी गाइड में MovieLens 100K डेटासेट का उपयोग करेंगे:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import pandas as pd
import numpy as np

# Load MovieLens 100K dataset
ratings = pd.read_csv(
    "https://files.grouplens.org/datasets/movielens/ml-100k/u.data",
    sep="\t",
    names=["user_id", "item_id", "rating", "timestamp"]
)

movies = pd.read_csv(
    "https://files.grouplens.org/datasets/movielens/ml-100k/u.item",
    sep="|",
    encoding="latin-1",
    names=["item_id", "title", "release_date", "video_release", "url",
           "unknown", "Action", "Adventure", "Animation", "Children",
           "Comedy", "Crime", "Documentary", "Drama", "Fantasy",
           "Film-Noir", "Horror", "Musical", "Mystery", "Romance",
           "Sci-Fi", "Thriller", "War", "Western"],
    usecols=range(24)
)

print(f"Ratings: {len(ratings)}")
print(f"Users: {ratings['user_id'].nunique()}")
print(f"Movies: {ratings['item_id'].nunique()}")
print(f"Sparsity: {1 - len(ratings) / (ratings['user_id'].nunique() * ratings['item_id'].nunique()):.4f}")

शुरुआत से कोलैबोरेटिव फ़िल्टरिंग

कोलैबोरेटिव फ़िल्टरिंग इस विचार पर काम करती है कि जिन उपयोगकर्ताओं ने अतीत में सहमति दिखाई, वे भविष्य में भी सहमत होंगे।

उपयोगकर्ता-आधारित कोलैबोरेटिव फ़िल्टरिंग

लक्षित उपयोगकर्ता के समान उपयोगकर्ताओं को खोजें, फिर वह सुझाएं जो उन समान उपयोगकर्ताओं ने पसंद किया:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import numpy as np
import pandas as pd
from scipy.spatial.distance import cosine

class UserBasedCF:
    def __init__(self, ratings_df: pd.DataFrame):
        self.ratings = ratings_df
        self.user_item_matrix = ratings_df.pivot_table(
            index="user_id", columns="item_id", values="rating"
        ).fillna(0)
        self.user_similarity = self._compute_similarity()

    def _compute_similarity(self) -> pd.DataFrame:
        """Compute cosine similarity between all users."""
        matrix = self.user_item_matrix.values
        n_users = matrix.shape[0]
        similarity = np.zeros((n_users, n_users))

        for i in range(n_users):
            for j in range(i, n_users):
                if np.any(matrix[i]) and np.any(matrix[j]):
                    sim = 1 - cosine(matrix[i], matrix[j])
                else:
                    sim = 0
                similarity[i][j] = sim
                similarity[j][i] = sim

        return pd.DataFrame(
            similarity,
            index=self.user_item_matrix.index,
            columns=self.user_item_matrix.index
        )

    def recommend(self, user_id: int, n_recommendations: int = 10, n_neighbors: int = 20) -> list:
        """Recommend items for a user based on similar users."""
        # Find most similar users
        similar_users = self.user_similarity[user_id].sort_values(ascending=False)
        similar_users = similar_users.iloc[1:n_neighbors + 1]  # Exclude self

        # Get items the user has not rated
        user_ratings = self.user_item_matrix.loc[user_id]
        unrated_items = user_ratings[user_ratings == 0].index

        # Predict ratings for unrated items
        predictions = {}
        for item in unrated_items:
            numerator = 0
            denominator = 0
            for neighbor_id, similarity in similar_users.items():
                neighbor_rating = self.user_item_matrix.loc[neighbor_id, item]
                if neighbor_rating > 0:
                    numerator += similarity * neighbor_rating
                    denominator += abs(similarity)

            if denominator > 0:
                predictions[item] = numerator / denominator

        # Sort and return top N
        sorted_predictions = sorted(predictions.items(), key=lambda x: x[1], reverse=True)
        return sorted_predictions[:n_recommendations]

# Usage
cf = UserBasedCF(ratings)
recommendations = cf.recommend(user_id=1, n_recommendations=10)
for item_id, predicted_rating in recommendations:
    title = movies[movies["item_id"] == item_id]["title"].values[0]
    print(f"  {title}: predicted rating {predicted_rating:.2f}")

आइटम-आधारित कोलैबोरेटिव फ़िल्टरिंग

समान उपयोगकर्ताओं को खोजने के बजाय, समान आइटम खोजें:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class ItemBasedCF:
    def __init__(self, ratings_df: pd.DataFrame):
        self.ratings = ratings_df
        self.user_item_matrix = ratings_df.pivot_table(
            index="user_id", columns="item_id", values="rating"
        ).fillna(0)
        self.item_similarity = self._compute_item_similarity()

    def _compute_item_similarity(self) -> pd.DataFrame:
        """Compute cosine similarity between items."""
        matrix = self.user_item_matrix.values.T  # Transpose: items as rows
        n_items = matrix.shape[0]
        similarity = np.zeros((n_items, n_items))

        for i in range(n_items):
            for j in range(i, n_items):
                if np.any(matrix[i]) and np.any(matrix[j]):
                    sim = 1 - cosine(matrix[i], matrix[j])
                else:
                    sim = 0
                similarity[i][j] = sim
                similarity[j][i] = sim

        return pd.DataFrame(
            similarity,
            index=self.user_item_matrix.columns,
            columns=self.user_item_matrix.columns
        )

    def recommend(self, user_id: int, n_recommendations: int = 10) -> list:
        """Recommend items based on item similarity."""
        user_ratings = self.user_item_matrix.loc[user_id]
        rated_items = user_ratings[user_ratings > 0]
        unrated_items = user_ratings[user_ratings == 0].index

        predictions = {}
        for item in unrated_items:
            numerator = 0
            denominator = 0
            for rated_item, rating in rated_items.items():
                sim = self.item_similarity.loc[item, rated_item]
                if sim > 0:
                    numerator += sim * rating
                    denominator += sim

            if denominator > 0:
                predictions[item] = numerator / denominator

        sorted_predictions = sorted(predictions.items(), key=lambda x: x[1], reverse=True)
        return sorted_predictions[:n_recommendations]

item_cf = ItemBasedCF(ratings)
recommendations = item_cf.recommend(user_id=1)
for item_id, score in recommendations:
    title = movies[movies["item_id"] == item_id]["title"].values[0]
    print(f"  {title}: {score:.2f}")

आइटम-आधारित CF को व्यवहार में आमतौर पर उपयोगकर्ता-आधारित की तुलना में प्राथमिकता दी जाती है। आइटम समानताएं उपयोगकर्ता समानताओं की तुलना में अधिक स्थिर होती हैं क्योंकि आइटम बदलते नहीं हैं, लेकिन उपयोगकर्ता प्राथमिकताएं बदलती हैं।

बड़े पैमाने पर रिकमेंडेशन के साथ काम करते हुए मैंने एक सबक सीखा कि आइटम समानताओं को ऑफ़लाइन पूर्व-गणना करके और उन्हें कैश से सर्व करने से लेटेंसी नाटकीय रूप से कम हो जाती है। एक प्रोजेक्ट में, समानता गणना को रिक्वेस्ट-टाइम से रात्रिकालीन बैच जॉब में स्थानांतरित करने से हमारा p95 रिस्पॉन्स टाइम 800ms से घटकर 50ms से नीचे आ गया, जिसने एक उपयोगी फ़ीचर और एक त्यागे गए फ़ीचर के बीच का अंतर बना दिया।

कंटेंट-आधारित फ़िल्टरिंग

कंटेंट-आधारित फ़िल्टरिंग आइटम विशेषताओं का उपयोग करके उपयोगकर्ता को पहले से पसंद आए आइटम के समान आइटम सुझाती है। यदि आपको पहले वेब से आइटम डेटा एकत्र करना है, तो व्यावहारिक तकनीकों के लिए हमारी Python वेब स्क्रैपिंग गाइड देखें।

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
from sklearn.metrics.pairwise import cosine_similarity

class ContentBasedRecommender:
    def __init__(self, movies_df: pd.DataFrame, ratings_df: pd.DataFrame):
        self.movies = movies_df
        self.ratings = ratings_df

        # Genre columns as feature vectors
        self.genre_columns = [
            "Action", "Adventure", "Animation", "Children", "Comedy",
            "Crime", "Documentary", "Drama", "Fantasy", "Film-Noir",
            "Horror", "Musical", "Mystery", "Romance", "Sci-Fi",
            "Thriller", "War", "Western"
        ]
        self.feature_matrix = self.movies[self.genre_columns].values
        self.item_similarity = cosine_similarity(self.feature_matrix)

    def get_user_profile(self, user_id: int) -> np.ndarray:
        """Build a user profile from their rated items."""
        user_ratings = self.ratings[self.ratings["user_id"] == user_id]
        user_ratings = user_ratings.merge(self.movies, on="item_id")

        # Weighted average of genre vectors by rating
        profile = np.zeros(len(self.genre_columns))
        total_weight = 0

        for _, row in user_ratings.iterrows():
            weight = row["rating"]
            features = row[self.genre_columns].values.astype(float)
            profile += weight * features
            total_weight += weight

        if total_weight > 0:
            profile /= total_weight

        return profile

    def recommend(self, user_id: int, n_recommendations: int = 10) -> list:
        """Recommend items based on content similarity to user profile."""
        user_profile = self.get_user_profile(user_id)

        # Compute similarity between user profile and all items
        scores = cosine_similarity([user_profile], self.feature_matrix)[0]

        # Get items the user has already rated
        rated_items = set(
            self.ratings[self.ratings["user_id"] == user_id]["item_id"].values
        )

        # Rank unrated items
        item_scores = []
        for idx, score in enumerate(scores):
            item_id = self.movies.iloc[idx]["item_id"]
            if item_id not in rated_items:
                item_scores.append((item_id, score))

        item_scores.sort(key=lambda x: x[1], reverse=True)
        return item_scores[:n_recommendations]

    def find_similar_movies(self, movie_id: int, n: int = 5) -> list:
        """Find movies similar to a given movie."""
        idx = self.movies[self.movies["item_id"] == movie_id].index[0]
        similarities = self.item_similarity[idx]

        similar_indices = similarities.argsort()[::-1][1:n + 1]
        results = []
        for i in similar_indices:
            results.append({
                "title": self.movies.iloc[i]["title"],
                "similarity": similarities[i],
                "genres": [g for g in self.genre_columns if self.movies.iloc[i][g] == 1]
            })
        return results

# Usage
cb = ContentBasedRecommender(movies, ratings)

# Get recommendations for a user
recs = cb.recommend(user_id=1)
print("Content-based recommendations for user 1:")
for item_id, score in recs:
    title = movies[movies["item_id"] == item_id]["title"].values[0]
    print(f"  {title}: score {score:.3f}")

# Find movies similar to "Toy Story (1995)"
similar = cb.find_similar_movies(movie_id=1)
print("\nMovies similar to Toy Story:")
for movie in similar:
    print(f"  {movie['title']} ({movie['similarity']:.3f}) - {', '.join(movie['genres'])}")

मैट्रिक्स फ़ैक्टराइज़ेशन

मैट्रिक्स फ़ैक्टराइज़ेशन स्पार्स उपयोगकर्ता-आइटम रेटिंग मैट्रिक्स को दो निम्न-रैंक मैट्रिक्स में विघटित करता है। यह लेटेंट फ़ैक्टर्स को कैप्चर करता है — छिपी हुई विशेषताएं जो रेटिंग पैटर्न को समझाती हैं।

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import numpy as np

class MatrixFactorization:
    def __init__(self, n_factors: int = 20, learning_rate: float = 0.005,
                 regularization: float = 0.02, n_epochs: int = 50):
        self.n_factors = n_factors
        self.lr = learning_rate
        self.reg = regularization
        self.n_epochs = n_epochs

    def fit(self, ratings_df: pd.DataFrame):
        """Train the model using SGD."""
        self.user_ids = sorted(ratings_df["user_id"].unique())
        self.item_ids = sorted(ratings_df["item_id"].unique())
        self.user_map = {uid: i for i, uid in enumerate(self.user_ids)}
        self.item_map = {iid: i for i, iid in enumerate(self.item_ids)}

        n_users = len(self.user_ids)
        n_items = len(self.item_ids)

        # Initialize latent factor matrices
        self.user_factors = np.random.normal(0, 0.1, (n_users, self.n_factors))
        self.item_factors = np.random.normal(0, 0.1, (n_items, self.n_factors))
        self.user_biases = np.zeros(n_users)
        self.item_biases = np.zeros(n_items)
        self.global_mean = ratings_df["rating"].mean()

        # Training with SGD
        ratings_list = ratings_df[["user_id", "item_id", "rating"]].values

        for epoch in range(self.n_epochs):
            np.random.shuffle(ratings_list)
            total_error = 0

            for user_id, item_id, rating in ratings_list:
                u = self.user_map[int(user_id)]
                i = self.item_map[int(item_id)]

                prediction = self.predict_single(u, i)
                error = rating - prediction
                total_error += error ** 2

                # Update biases
                self.user_biases[u] += self.lr * (error - self.reg * self.user_biases[u])
                self.item_biases[i] += self.lr * (error - self.reg * self.item_biases[i])

                # Update latent factors
                user_factor = self.user_factors[u].copy()
                self.user_factors[u] += self.lr * (error * self.item_factors[i] - self.reg * self.user_factors[u])
                self.item_factors[i] += self.lr * (error * user_factor - self.reg * self.item_factors[i])

            rmse = np.sqrt(total_error / len(ratings_list))
            if (epoch + 1) % 10 == 0:
                print(f"Epoch {epoch + 1}/{self.n_epochs}, RMSE: {rmse:.4f}")

    def predict_single(self, user_idx: int, item_idx: int) -> float:
        """Predict rating for a user-item pair (using internal indices)."""
        return (self.global_mean +
                self.user_biases[user_idx] +
                self.item_biases[item_idx] +
                np.dot(self.user_factors[user_idx], self.item_factors[item_idx]))

    def predict(self, user_id: int, item_id: int) -> float:
        """Predict rating for a user-item pair (using original IDs)."""
        if user_id not in self.user_map or item_id not in self.item_map:
            return self.global_mean
        u = self.user_map[user_id]
        i = self.item_map[item_id]
        return np.clip(self.predict_single(u, i), 1, 5)

    def recommend(self, user_id: int, rated_items: set, n: int = 10) -> list:
        """Get top-N recommendations for a user."""
        if user_id not in self.user_map:
            return []

        predictions = []
        for item_id in self.item_ids:
            if item_id not in rated_items:
                score = self.predict(user_id, item_id)
                predictions.append((item_id, score))

        predictions.sort(key=lambda x: x[1], reverse=True)
        return predictions[:n]

# Train
mf = MatrixFactorization(n_factors=20, n_epochs=50)
mf.fit(ratings)

# Get recommendations
user_rated = set(ratings[ratings["user_id"] == 1]["item_id"].values)
recs = mf.recommend(user_id=1, rated_items=user_rated)

print("Matrix factorization recommendations:")
for item_id, score in recs:
    title = movies[movies["item_id"] == item_id]["title"].values[0]
    print(f"  {title}: {score:.2f}")

Surprise लाइब्रेरी का उपयोग

Surprise लाइब्रेरी सामान्य रिकमेंडेशन एल्गोरिदम के ऑप्टिमाइज़्ड इम्प्लीमेंटेशन प्रदान करती है:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from surprise import Dataset, Reader, SVD, KNNBasic, NMF
from surprise.model_selection import cross_validate, train_test_split
from surprise import accuracy
import pandas as pd

# Load data into Surprise format
reader = Reader(rating_scale=(1, 5))
data = Dataset.load_from_df(ratings[["user_id", "item_id", "rating"]], reader)

# Compare different algorithms
algorithms = {
    "SVD": SVD(n_factors=50, n_epochs=20, lr_all=0.005, reg_all=0.02),
    "KNN User-Based": KNNBasic(k=40, sim_options={"name": "cosine", "user_based": True}),
    "KNN Item-Based": KNNBasic(k=40, sim_options={"name": "cosine", "user_based": False}),
    "NMF": NMF(n_factors=15, n_epochs=50),
}

for name, algo in algorithms.items():
    results = cross_validate(algo, data, measures=["RMSE", "MAE"], cv=5, verbose=False)
    print(f"{name:20s} RMSE: {results['test_rmse'].mean():.4f}  MAE: {results['test_mae'].mean():.4f}")

सर्वश्रेष्ठ मॉडल को ट्रेन करें और रिकमेंडेशन जेनरेट करें:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
from surprise import SVD, Dataset, Reader
from collections import defaultdict

reader = Reader(rating_scale=(1, 5))
data = Dataset.load_from_df(ratings[["user_id", "item_id", "rating"]], reader)
trainset = data.build_full_trainset()

model = SVD(n_factors=50, n_epochs=20, lr_all=0.005, reg_all=0.02)
model.fit(trainset)

def get_top_n(model, trainset, n=10):
    """Get top N recommendations for all users."""
    # Get all item IDs
    all_items = set(trainset.all_items())
    top_n = defaultdict(list)

    for uid in trainset.all_users():
        raw_uid = trainset.to_raw_uid(uid)
        # Items the user has already rated
        rated_items = set(trainset.ur[uid])
        rated_iids = {iid for iid, _ in rated_items}

        # Predict for unrated items
        for iid in all_items:
            if iid not in rated_iids:
                raw_iid = trainset.to_raw_iid(iid)
                prediction = model.predict(raw_uid, raw_iid)
                top_n[raw_uid].append((raw_iid, prediction.est))

        # Sort and keep top N
        top_n[raw_uid].sort(key=lambda x: x[1], reverse=True)
        top_n[raw_uid] = top_n[raw_uid][:n]

    return top_n

top_n = get_top_n(model, trainset, n=10)

# Show recommendations for user 1
print("Top 10 for user 1:")
for item_id, predicted_rating in top_n[1]:
    title = movies[movies["item_id"] == item_id]["title"].values[0]
    print(f"  {title}: {predicted_rating:.2f}")

हाइब्रिड रिकमेंडेशन सिस्टम

बेहतर परिणामों के लिए कोलैबोरेटिव और कंटेंट-आधारित दृष्टिकोणों को संयोजित करें:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
from sklearn.preprocessing import MinMaxScaler

class HybridRecommender:
    def __init__(self, ratings_df: pd.DataFrame, movies_df: pd.DataFrame,
                 cf_weight: float = 0.6, cb_weight: float = 0.4):
        self.cf_weight = cf_weight
        self.cb_weight = cb_weight
        self.ratings = ratings_df
        self.movies = movies_df
        self.scaler = MinMaxScaler()

        # Train collaborative filtering (using Surprise SVD)
        from surprise import SVD, Dataset, Reader
        reader = Reader(rating_scale=(1, 5))
        data = Dataset.load_from_df(ratings_df[["user_id", "item_id", "rating"]], reader)
        trainset = data.build_full_trainset()
        self.cf_model = SVD(n_factors=50, n_epochs=20)
        self.cf_model.fit(trainset)
        self.trainset = trainset

        # Build content-based features
        self.genre_columns = [
            "Action", "Adventure", "Animation", "Children", "Comedy",
            "Crime", "Documentary", "Drama", "Fantasy", "Film-Noir",
            "Horror", "Musical", "Mystery", "Romance", "Sci-Fi",
            "Thriller", "War", "Western"
        ]
        self.feature_matrix = movies_df[self.genre_columns].values
        self.item_similarity = cosine_similarity(self.feature_matrix)

    def _cf_score(self, user_id: int, item_id: int) -> float:
        """Get collaborative filtering prediction."""
        return self.cf_model.predict(user_id, item_id).est

    def _cb_score(self, user_id: int, item_id: int) -> float:
        """Get content-based score."""
        user_ratings = self.ratings[self.ratings["user_id"] == user_id]
        if len(user_ratings) == 0:
            return 0

        target_idx = self.movies[self.movies["item_id"] == item_id].index
        if len(target_idx) == 0:
            return 0
        target_idx = target_idx[0]

        score = 0
        weight_sum = 0
        for _, row in user_ratings.iterrows():
            rated_idx = self.movies[self.movies["item_id"] == row["item_id"]].index
            if len(rated_idx) == 0:
                continue
            rated_idx = rated_idx[0]
            sim = self.item_similarity[target_idx][rated_idx]
            score += sim * row["rating"]
            weight_sum += sim

        return score / weight_sum if weight_sum > 0 else 0

    def recommend(self, user_id: int, n: int = 10) -> list:
        """Generate hybrid recommendations."""
        rated_items = set(self.ratings[self.ratings["user_id"] == user_id]["item_id"].values)
        all_items = self.movies["item_id"].values

        scores = []
        for item_id in all_items:
            if item_id in rated_items:
                continue
            cf = self._cf_score(user_id, item_id)
            cb = self._cb_score(user_id, item_id)
            hybrid = self.cf_weight * cf + self.cb_weight * cb
            scores.append((item_id, hybrid, cf, cb))

        scores.sort(key=lambda x: x[1], reverse=True)
        return scores[:n]

# Usage
hybrid = HybridRecommender(ratings, movies, cf_weight=0.6, cb_weight=0.4)
recs = hybrid.recommend(user_id=1, n=10)

print("Hybrid recommendations for user 1:")
print(f"{'Title':<45} {'Hybrid':>7} {'CF':>7} {'CB':>7}")
print("-" * 70)
for item_id, hybrid_score, cf_score, cb_score in recs:
    title = movies[movies["item_id"] == item_id]["title"].values[0]
    print(f"{title:<45} {hybrid_score:>7.2f} {cf_score:>7.2f} {cb_score:>7.2f}")

रिकमेंडेशन सिस्टम का मूल्यांकन

मूल्यांकन RMSE से आगे जाता है। समान मूल्यांकन चुनौतियां फ्रॉड डिटेक्शन में भी उत्पन्न होती हैं, जहां क्लास असंतुलन मानक एक्यूरेसी को भ्रामक बना देता है। रिकमेंडेशन वास्तव में कितने अच्छे हैं यह मापने के लिए रैंकिंग मेट्रिक्स का उपयोग करें:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
from sklearn.model_selection import train_test_split
import numpy as np

def evaluate_recommender(model, ratings_df, k=10):
    """Evaluate with precision@k and recall@k."""
    train_df, test_df = train_test_split(ratings_df, test_size=0.2, random_state=42)

    # Group test ratings by user
    test_user_items = test_df.groupby("user_id")["item_id"].apply(set).to_dict()
    # Only consider items rated 4+ as "relevant"
    relevant_items = test_df[test_df["rating"] >= 4].groupby("user_id")["item_id"].apply(set).to_dict()

    precisions = []
    recalls = []

    for user_id in test_user_items:
        if user_id not in relevant_items or len(relevant_items[user_id]) == 0:
            continue

        # Get top-k recommendations
        rated_in_train = set(train_df[train_df["user_id"] == user_id]["item_id"].values)
        recs = model.recommend(user_id, rated_items=rated_in_train, n=k)
        rec_items = set(item_id for item_id, _ in recs)

        # Precision: fraction of recommended items that are relevant
        hits = rec_items & relevant_items[user_id]
        precision = len(hits) / k if k > 0 else 0
        precisions.append(precision)

        # Recall: fraction of relevant items that were recommended
        recall = len(hits) / len(relevant_items[user_id])
        recalls.append(recall)

    avg_precision = np.mean(precisions)
    avg_recall = np.mean(recalls)

    print(f"Precision@{k}: {avg_precision:.4f}")
    print(f"Recall@{k}: {avg_recall:.4f}")
    print(f"F1@{k}: {2 * avg_precision * avg_recall / (avg_precision + avg_recall + 1e-8):.4f}")

    return avg_precision, avg_recall

कोल्ड स्टार्ट समाधान

नए उपयोगकर्ताओं और नए आइटम के पास कोई इंटरैक्शन इतिहास नहीं होता। यहां कुछ व्यावहारिक दृष्टिकोण दिए गए हैं:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def popularity_recommendations(ratings_df: pd.DataFrame, n: int = 10) -> list:
    """Recommend most popular items as a fallback."""
    popular = (ratings_df.groupby("item_id")
               .agg(avg_rating=("rating", "mean"), count=("rating", "count"))
               .query("count >= 20")
               .sort_values("avg_rating", ascending=False)
               .head(n))
    return popular.index.tolist()

def demographic_recommendations(user_features: dict, user_db: pd.DataFrame,
                                ratings_df: pd.DataFrame, n: int = 10) -> list:
    """Recommend based on what similar demographic groups liked."""
    # Find users in similar age/gender group
    similar_users = user_db[
        (user_db["age_group"] == user_features["age_group"]) &
        (user_db["gender"] == user_features["gender"])
    ]["user_id"].values

    # Get their highly-rated items
    group_ratings = ratings_df[
        (ratings_df["user_id"].isin(similar_users)) &
        (ratings_df["rating"] >= 4)
    ]

    popular_in_group = (group_ratings.groupby("item_id")["rating"]
                        .count()
                        .sort_values(ascending=False)
                        .head(n))

    return popular_in_group.index.tolist()

सारांश

रिकमेंडेशन सिस्टम जटिलता की एक स्पष्ट प्रगति का अनुसरण करते हैं:

  1. लोकप्रियता-आधारित — सबसे सरल बेसलाइन। जो सबसे लोकप्रिय है उसे सुझाएं। कोल्ड-स्टार्ट फ़ॉलबैक के रूप में काम करता है।
  2. कंटेंट-आधारित फ़िल्टरिंग — आइटम विशेषताओं का उपयोग करती है। तब अच्छी होती है जब आपके पास समृद्ध मेटाडेटा हो। अन्य उपयोगकर्ताओं के डेटा की आवश्यकता नहीं होती।
  3. कोलैबोरेटिव फ़िल्टरिंग — इंटरैक्शन पैटर्न का उपयोग करती है। गैर-स्पष्ट रिकमेंडेशन खोजती है। पर्याप्त उपयोगकर्ता-आइटम इंटरैक्शन की आवश्यकता होती है।
  4. मैट्रिक्स फ़ैक्टराइज़ेशन — रेटिंग मैट्रिक्स से लेटेंट फ़ैक्टर्स सीखता है। कच्ची कोलैबोरेटिव फ़िल्टरिंग की तुलना में बेहतर जनरलाइज़ेशन।
  5. हाइब्रिड — कई दृष्टिकोणों को संयोजित करता है। समग्र रूप से सर्वोत्तम प्रदर्शन।

मज़बूत बेसलाइन के लिए Surprise लाइब्रेरी के SVD से शुरुआत करें। यदि आपके पास अच्छा आइटम मेटाडेटा है तो कंटेंट-आधारित विशेषताएं जोड़ें। कोल्ड-स्टार्ट उपयोगकर्ताओं के लिए लोकप्रियता को फ़ॉलबैक के रूप में उपयोग करें। केवल RMSE के बजाय precision@k और recall@k जैसी रैंकिंग मेट्रिक्स के साथ मूल्यांकन करें, क्योंकि वास्तविक रिकमेंडेशन गुणवत्ता इस बारे में है कि उपयोगकर्ता शीर्ष कुछ सुझावों को पसंद करते हैं या नहीं, न कि सभी आइटम में औसत भविष्यवाणी त्रुटि के बारे में।

संबंधित पोस्ट

Khushal Jethava
Khushal Jethava

Machine Learning Engineer at Codiste, specializing in Generative AI, NLP, and Computer Vision. Building production AI systems with Python.

This post is licensed under CC BY 4.0 by the author.