代码示例 / 结构化数据 / 电影推荐的协同过滤

电影推荐的协同过滤

作者: Siddhartha Banerjee
创建日期: 2020/05/24
最后修改: 2020/05/24
描述: 使用在Movielens数据集上训练的模型进行电影推荐。

在Colab中查看 GitHub源代码


介绍

本示例演示了使用 Movielens 数据集协同过滤 来向用户推荐电影。 MovieLens 评级数据集列出了用户对一组电影给出的评级。 我们的目标是能够预测用户尚未观看的电影的评级。 然后可以向用户推荐预测评级最高的电影。

模型的步骤如下:

  1. 通过嵌入矩阵将用户ID映射到“用户向量”
  2. 通过嵌入矩阵将电影ID映射到“电影向量”
  3. 计算用户向量和电影向量之间的点积,以获得用户与电影之间的匹配分数(预测评级)。
  4. 使用所有已知的用户-电影对通过梯度下降训练嵌入。

参考文献:

import pandas as pd
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
from zipfile import ZipFile

import keras
from keras import layers
from keras import ops

首先,加载数据并进行预处理

# 从 http://files.grouplens.org/datasets/movielens/ml-latest-small.zip 下载实际数据
# 使用 ratings.csv 文件
movielens_data_file_url = (
    "http://files.grouplens.org/datasets/movielens/ml-latest-small.zip"
)
movielens_zipped_file = keras.utils.get_file(
    "ml-latest-small.zip", movielens_data_file_url, extract=False
)
keras_datasets_path = Path(movielens_zipped_file).parents[0]
movielens_dir = keras_datasets_path / "ml-latest-small"

# 仅在脚本首次运行时提取数据。
if not movielens_dir.exists():
    with ZipFile(movielens_zipped_file, "r") as zip:
        # 提取文件
        print("正在提取所有文件...")
        zip.extractall(path=keras_datasets_path)
        print("完成!")

ratings_file = movielens_dir / "ratings.csv"
df = pd.read_csv(ratings_file)
从 http://files.grouplens.org/datasets/movielens/ml-latest-small.zip 下载数据
 978202/978202 ━━━━━━━━━━━━━━━━━━━━ 0s 0us/step
正在提取所有文件...
完成!

首先,需要进行一些预处理以将用户和电影编码为整数索引。

user_ids = df["userId"].unique().tolist()
user2user_encoded = {x: i for i, x in enumerate(user_ids)}
userencoded2user = {i: x for i, x in enumerate(user_ids)}
movie_ids = df["movieId"].unique().tolist()
movie2movie_encoded = {x: i for i, x in enumerate(movie_ids)}
movie_encoded2movie = {i: x for i, x in enumerate(movie_ids)}
df["user"] = df["userId"].map(user2user_encoded)
df["movie"] = df["movieId"].map(movie2movie_encoded)

num_users = len(user2user_encoded)
num_movies = len(movie_encoded2movie)
df["rating"] = df["rating"].values.astype(np.float32)
# 最低和最高评级将在后面用于标准化评级
min_rating = min(df["rating"])
max_rating = max(df["rating"])

print(
    "用户数量: {}, 电影数量: {}, 最低评级: {}, 最高评级: {}".format(
        num_users, num_movies, min_rating, max_rating
    )
)
用户数量: 610, 电影数量: 9724, 最低评级: 0.5, 最高评级: 5.0

准备训练和验证数据

df = df.sample(frac=1, random_state=42)
x = df[["user", "movie"]].values
# 将目标标准化到0和1之间。易于训练。
y = df["rating"].apply(lambda x: (x - min_rating) / (max_rating - min_rating)).values
# 假设在90%的数据上进行训练,在10%的数据上进行验证。
train_indices = int(0.9 * df.shape[0])
x_train, x_val, y_train, y_val = (
    x[:train_indices],
    x[train_indices:],
    y[:train_indices],
    y[train_indices:],
)

创建模型

我们将用户和电影嵌入到50维向量中。

该模型通过点积计算用户和电影嵌入之间的匹配分数,并增加每个电影和每个用户的偏差。匹配分数被缩放到 [0, 1] interval 通过 sigmoid (因为我们的评分被归一化到这个范围)。

EMBEDDING_SIZE = 50


class RecommenderNet(keras.Model):
    def __init__(self, num_users, num_movies, embedding_size, **kwargs):
        super().__init__(**kwargs)
        self.num_users = num_users
        self.num_movies = num_movies
        self.embedding_size = embedding_size
        self.user_embedding = layers.Embedding(
            num_users,
            embedding_size,
            embeddings_initializer="he_normal",
            embeddings_regularizer=keras.regularizers.l2(1e-6),
        )
        self.user_bias = layers.Embedding(num_users, 1)
        self.movie_embedding = layers.Embedding(
            num_movies,
            embedding_size,
            embeddings_initializer="he_normal",
            embeddings_regularizer=keras.regularizers.l2(1e-6),
        )
        self.movie_bias = layers.Embedding(num_movies, 1)

    def call(self, inputs):
        user_vector = self.user_embedding(inputs[:, 0])
        user_bias = self.user_bias(inputs[:, 0])
        movie_vector = self.movie_embedding(inputs[:, 1])
        movie_bias = self.movie_bias(inputs[:, 1])
        dot_user_movie = ops.tensordot(user_vector, movie_vector, 2)
        # 添加所有组成部分(包括偏差)
        x = dot_user_movie + user_bias + movie_bias
        # Sigmoid 激活将评分强制限制在 0 和 1 之间
        return ops.nn.sigmoid(x)


model = RecommenderNet(num_users, num_movies, EMBEDDING_SIZE)
model.compile(
    loss=keras.losses.BinaryCrossentropy(),
    optimizer=keras.optimizers.Adam(learning_rate=0.001),
)

根据数据拆分训练模型

history = model.fit(
    x=x_train,
    y=y_train,
    batch_size=64,
    epochs=5,
    verbose=1,
    validation_data=(x_val, y_val),
)
Epoch 1/5
 1418/1418 ━━━━━━━━━━━━━━━━━━━━ 2s 1ms/step - loss: 0.6591 - val_loss: 0.6201
Epoch 2/5
 1418/1418 ━━━━━━━━━━━━━━━━━━━━ 1s 894us/step - loss: 0.6159 - val_loss: 0.6191
Epoch 3/5
 1418/1418 ━━━━━━━━━━━━━━━━━━━━ 1s 977us/step - loss: 0.6093 - val_loss: 0.6138
Epoch 4/5
 1418/1418 ━━━━━━━━━━━━━━━━━━━━ 1s 865us/step - loss: 0.6100 - val_loss: 0.6123
Epoch 5/5
 1418/1418 ━━━━━━━━━━━━━━━━━━━━ 1s 854us/step - loss: 0.6072 - val_loss: 0.6121

绘制训练和验证损失

plt.plot(history.history["loss"])
plt.plot(history.history["val_loss"])
plt.title("模型损失")
plt.ylabel("损失")
plt.xlabel("轮次")
plt.legend(["训练", "测试"], loc="upper left")
plt.show()

png


向用户展示前 10 个电影推荐

movie_df = pd.read_csv(movielens_dir / "movies.csv")

# 让我们获取一位用户并查看前面的推荐。
user_id = df.userId.sample(1).iloc[0]
movies_watched_by_user = df[df.userId == user_id]
movies_not_watched = movie_df[
    ~movie_df["movieId"].isin(movies_watched_by_user.movieId.values)
]["movieId"]
movies_not_watched = list(
    set(movies_not_watched).intersection(set(movie2movie_encoded.keys()))
)
movies_not_watched = [[movie2movie_encoded.get(x)] for x in movies_not_watched]
user_encoder = user2user_encoded.get(user_id)
user_movie_array = np.hstack(
    ([[user_encoder]] * len(movies_not_watched), movies_not_watched)
)
ratings = model.predict(user_movie_array).flatten()
top_ratings_indices = ratings.argsort()[-10:][::-1]
recommended_movie_ids = [
    movie_encoded2movie.get(movies_not_watched[x][0]) for x in top_ratings_indices
]

print("显示用户的推荐:{}".format(user_id))
print("====" * 9)
print("用户的高评分电影")
print("----" * 8)
top_movies_user = (
    movies_watched_by_user.sort_values(by="rating", ascending=False)
    .head(5)
    .movieId.values
)
movie_df_rows = movie_df[movie_df["movieId"].isin(top_movies_user)]
for row in movie_df_rows.itertuples():
    print(row.title, ":", row.genres)

print("----" * 8)
print("前 10 个电影推荐")
print("----" * 8)
recommended_movies = movie_df[movie_df["movieId"].isin(recommended_movie_ids)]
for row in recommended_movies.itertuples():
    print(row.title, ":", row.genres)
 272/272 ━━━━━━━━━━━━━━━━━━━━ 0s 714us/step
为用户:249 显示推荐
====================================
用户评分较高的电影
--------------------------------
搏击俱乐部 (1999) : 动作|犯罪|剧情|惊悚
宁静 (2005) : 动作|冒险|科幻
无间道 (2006) : 犯罪|剧情|惊悚
囚徒 (2013) : 剧情|悬疑|惊悚
降临 (2016) : 科幻
--------------------------------
前 10 名电影推荐
--------------------------------
父亲的名字 (1993) : 剧情
蒙提·派森与圣杯 (1975) : 冒险|喜剧|奇幻
公主新娘 (1987) : 动作|冒险|喜剧|奇幻|浪漫
阿拉伯的劳伦斯 (1962) : 冒险|剧情|战争
现代启示录 (1979) : 动作|剧情|战争
全金属外壳 (1987) : 剧情|战争
莫扎特 (1984) : 剧情
荣耀 (1989) : 剧情|战争
唐人街 (1974) : 犯罪|黑色电影|悬疑|惊悚
上帝之城 (Cidade de Deus) (2002) : 动作|冒险|犯罪|剧情|惊悚