使用协同过滤与Qdrant构建电影推荐系统

时间: 45 分钟等级: 中级Open In Colab

每次Spotify推荐你从未听说过的乐队的下一首歌时,它都会使用基于其他用户与该歌曲互动的推荐算法。这种类型的算法被称为协同过滤

与基于内容的推荐不同,当对象的语义与用户的偏好松散或不相关时,协同过滤表现出色。这种适应性使其非常引人入胜。电影、音乐或书籍推荐是此类用例的良好示例。毕竟,我们很少纯粹基于情节转折来选择阅读哪本书。

构建协同过滤引擎的传统方法涉及训练一个模型,该模型将用户到项目关系的稀疏矩阵转换为用户和项目向量的压缩、密集表示。为此目的,一些最常被引用的算法包括SVD(奇异值分解)因子分解机。然而,模型训练方法需要大量的资源投入。模型训练需要数据、定期重新训练和成熟的基础设施。

方法论

幸运的是,有一种方法可以在不进行任何模型训练的情况下构建协同过滤系统。您可以通过使用基于相似性搜索的技术获得可解释的推荐,并拥有一个可扩展的系统。让我们通过一个构建电影推荐系统的例子来探索这是如何工作的。

实现

为了实现这一点,您将使用一个简单而强大的资源:Qdrant与稀疏向量

笔记本:你可以在这里尝试这段代码

设置

首先,您需要导入必要的库并定义环境。

import os
import pandas as pd
import requests
from qdrant_client import QdrantClient, models
from qdrant_client.models import PointStruct, SparseVector, NamedSparseVector
from collections import defaultdict

# OMDB API Key - for movie posters
omdb_api_key = os.getenv("OMDB_API_KEY")

# Collection name
collection_name = "movies"

# Set Qdrant Client
qdrant_client = QdrantClient(
    os.getenv("QDRANT_HOST"),
    api_key=os.getenv("QDRANT_API_KEY")
)

定义输出

在这里,您将配置推荐引擎以检索电影海报作为输出。

# Function to get movie poster using OMDB API
def get_movie_poster(imdb_id, api_key):
    url = f"https://www.omdbapi.com/?i={imdb_id}&apikey={api_key}"
    data = requests.get(url).json()
    return data.get('Poster'), data

准备数据

加载电影数据集。这些包括三个主要的CSV文件:用户评分、电影标题和OMDB ID。

# Load CSV files
ratings_df = pd.read_csv('data/ratings.csv', low_memory=False)
movies_df = pd.read_csv('data/movies.csv', low_memory=False)

# Convert movieId in ratings_df and movies_df to string
ratings_df['movieId'] = ratings_df['movieId'].astype(str)
movies_df['movieId'] = movies_df['movieId'].astype(str)

rating = ratings_df['rating']

# Normalize ratings
ratings_df['rating'] = (rating - rating.mean()) / rating.std()

# Merge ratings with movie metadata to get movie titles
merged_df = ratings_df.merge(
    movies_df[['movieId', 'title']],
    left_on='movieId', right_on='movieId', how='inner'
)

# Aggregate ratings to handle duplicate (userId, title) pairs
ratings_agg_df = merged_df.groupby(['userId', 'movieId']).rating.mean().reset_index()

ratings_agg_df.head()
用户ID电影ID评分
0110.429960
1110361.369846
211049-0.509926
3110660.429960
411100.429960

转换为稀疏

如果你想搜索来自不同用户的大量评论,你可以将这些评论表示为一个稀疏矩阵。

# Convert ratings to sparse vectors
user_sparse_vectors = defaultdict(lambda: {"values": [], "indices": []})
for row in ratings_agg_df.itertuples():
    user_sparse_vectors[row.userId]["values"].append(row.rating)
    user_sparse_vectors[row.userId]["indices"].append(int(row.movieId))

collaborative-filtering

上传数据

在这里,您将初始化Qdrant客户端并创建一个新的集合来存储数据。 将用户评分转换为稀疏向量,并在有效载荷中包含movieId

# Define a data generator
def data_generator():
    for user_id, sparse_vector in user_sparse_vectors.items():
        yield PointStruct(
            id=user_id,
            vector={"ratings": SparseVector(
                indices=sparse_vector["indices"],
                values=sparse_vector["values"]
            )},
            payload={"user_id": user_id, "movie_id": sparse_vector["indices"]}
        )

# Upload points using the data generator
qdrant_client.upload_points(
    collection_name=collection_name,
    points=data_generator()
)

定义查询

为了获得推荐,我们需要找到与我们口味相似的用户。 让我们通过为我们最喜欢的一些电影提供评分来描述我们的偏好。

1 表示我们喜欢这部电影,-1 表示我们不喜欢它。

my_ratings = {
    603: 1,     # Matrix
    13475: 1,   # Star Trek
    11: 1,      # Star Wars
    1091: -1,   # The Thing
    862: 1,     # Toy Story
    597: -1,    # Titanic
    680: -1,    # Pulp Fiction
    13: 1,      # Forrest Gump
    120: 1,     # Lord of the Rings
    87: -1,     # Indiana Jones
    562: -1     # Die Hard
}
Click to see the code for to_vector
# Create sparse vector from my_ratings
def to_vector(ratings):
    vector = SparseVector(
        values=[],
        indices=[]
    )
    for movie_id, rating in ratings.items():
        vector.values.append(rating)
        vector.indices.append(movie_id)
    return vector

运行查询

从上传的带有评分的电影列表中,我们可以在Qdrant中执行搜索,以获取与我们最相似的用户。

# Perform the search
results = qdrant_client.query_points(
    collection_name=collection_name,
    query=to_vector(my_ratings),
    using="ratings",
    limit=20
).points

现在我们可以找到其他相似用户喜欢但我们尚未观看的电影。 让我们结合找到的用户的结果,过滤掉已观看的电影,并按评分排序。

# Convert results to scores and sort by score
def results_to_scores(results):
    movie_scores = defaultdict(lambda: 0)
    for result in results:
        for movie_id in result.payload["movie_id"]:
            movie_scores[movie_id] += result.score
    return movie_scores

# Convert results to scores and sort by score
movie_scores = results_to_scores(results)
top_movies = sorted(movie_scores.items(), key=lambda x: x[1], reverse=True)
Visualize results in Jupyter Notebook

最后,我们展示了前5部推荐的电影及其海报和标题。

# Create HTML to display top 5 results
html_content = "<div class='movies-container'>"

for movie_id, score in top_movies[:5]:
    imdb_id_row = links.loc[links['movieId'] == int(movie_id), 'imdbId']
    if not imdb_id_row.empty:
        imdb_id = imdb_id_row.values[0]
        poster_url, movie_info = get_movie_poster(imdb_id, omdb_api_key)
        movie_title = movie_info.get('Title', 'Unknown Title')
        
        html_content += f"""
        <div class='movie-card'>
            <img src="{poster_url}" alt="Poster" class="movie-poster">
            <div class="movie-title">{movie_title}</div>
            <div class="movie-score">Score: {score}</div>
        </div>
        """
    else:
        continue  # Skip if imdb_id is not found

html_content += "</div>"

display(HTML(html_content))

推荐

要查看电影海报的完整展示,请查看笔记本输出。以下是不包含html内容的结果。

Toy Story, Score: 131.2033799 
Monty Python and the Holy Grail, Score: 131.2033799 
Star Wars: Episode V - The Empire Strikes Back, Score: 131.2033799  
Star Wars: Episode VI - Return of the Jedi, Score: 131.2033799 
Men in Black, Score: 131.2033799

在协同过滤的基础上,我们可以通过整合其他特征如用户人口统计、电影类型或电影标签来进一步增强推荐系统。

或者,例如,仅通过基于时间的过滤器考虑最近的评分。这样,我们可以推荐当前在用户中流行的电影。

结论

正如所展示的,使用Qdrant和稀疏向量可以构建一个有趣的电影推荐系统,而无需进行密集的模型训练。这种方法不仅简化了推荐过程,还使其具有可扩展性和可解释性。在未来的教程中,我们可以进一步尝试这种组合,以进一步增强我们的推荐系统。

这个页面有用吗?

感谢您的反馈!🙏

我们很抱歉听到这个消息。😔 你可以在GitHub上编辑这个页面,或者创建一个GitHub问题。