使用协同过滤和 Qdrant 构建电影推荐系统
每次 Spotify 推荐您从未听过的乐队的下一首歌曲时,它都使用了基于其他用户与该歌曲互动情况的推荐算法。这种算法被称为 协同过滤。
与基于内容的推荐不同,当对象的语义与用户的偏好关联较弱或无关时,协同过滤表现出色。这种适应性使其如此引人入胜。电影、音乐或书籍推荐就是这种用例的良好范例。毕竟,我们很少完全基于情节来选择要阅读的书。
构建协同过滤引擎的传统方法涉及训练一个模型,将用户与物品关系的稀疏矩阵转换为用户和物品向量的压缩、密集表示。为此,最常用的算法包括 SVD(奇异值分解) 和 矩阵分解。然而,模型训练方法需要大量的资源投入。模型训练需要数据、定期重新训练以及成熟的基础设施。
方法
幸运的是,有一种方法可以在不进行任何模型训练的情况下构建协同过滤系统。您可以使用基于相似度搜索的技术获得可解释的推荐和可扩展的系统。让我们通过构建电影推荐系统的示例来探讨其工作原理。
实现
为了实现这一点,您将使用一个简单而强大的资源:带有稀疏向量的 Qdrant。
Notebook:您可以在此处尝试此代码
设置
您必须首先导入必要的库并定义环境。
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 | 评分 | |
---|---|---|---|
0 | 1 | 1 | 0.429960 |
1 | 1 | 1036 | 1.369846 |
2 | 1 | 1049 | -0.509926 |
3 | 1 | 1066 | 0.429960 |
4 | 1 | 110 | 0.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))
上传数据
在此,您将初始化 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
}
点击查看 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)
在 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))
推荐
要查看完整的电影海报展示,请查看 notebook 输出。以下是没有 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 和稀疏向量构建一个有趣的电影推荐系统,而无需进行密集的模型训练。这种方法不仅简化了推荐过程,而且使其具有可扩展性和可解释性。在未来的教程中,我们可以进一步尝试这种组合,以进一步增强我们的推荐系统。