案例系列:Movielens

您所在的位置:网站首页 电影评分最高的电影是什么 案例系列:Movielens

案例系列:Movielens

2024-06-03 04:40| 来源: 网络整理| 查看: 265

文章目录 简介数据集设置准备数据下载并准备数据框将电影评分数据转换为序列 定义元数据为训练和评估创建 `tf.data.Dataset`创建模型输入编码输入特征创建一个二叉搜索树模型运行训练和评估实验结论 描述: 使用行为序列Transformer(BST)模型在Movielens上进行评分预测。

简介

本示例演示了由Qiwei Chen等人使用Movielens数据集使用行为序列转换器(BST)模型。BST模型利用用户在观看和评分电影时的顺序行为,以及用户配置文件和电影特征,来预测用户对目标电影的评分。

更具体地说,BST模型旨在通过接受以下输入来预测目标电影的评分:

用户观看的电影的固定长度的序列,其中包含movie_ids。用户观看的电影的固定长度的序列,其中包含电影的ratings。用户特征的集合,包括user_id、sex、occupation和age_group。输入序列和目标电影中每个电影的genres的集合。要预测评分的target_movie_id。

本示例对原始BST模型进行了以下修改:

我们将电影特征(genres)合并到每个输入序列和目标电影的嵌入处理中,而不是将它们视为转换器层外的“其他特征”。我们利用输入序列中电影的评分以及它们在序列中的位置,在将它们馈送到自注意力层之前对它们进行更新。

请注意,此示例应在TensorFlow 2.4或更高版本上运行。

数据集

我们使用Movielens数据集的1M版本。 该数据集包括来自6000个用户对4000部电影的大约100万个评分, 还包括一些用户特征和电影类型。此外,还提供了每个用户-电影评分的时间戳, 这允许为每个用户创建电影评分序列,正如BST模型所期望的那样。

设置 # 导入所需的库 import os # 用于操作系统相关的功能 os.environ["KERAS_BACKEND"] = "tensorflow" # 设置环境变量,指定使用tensorflow作为Keras的后端 import math # 用于数学计算 from zipfile import ZipFile # 用于解压缩zip文件 from urllib.request import urlretrieve # 用于从URL下载文件 import keras # Keras库,用于构建深度学习模型 import numpy as np # 用于处理数值数组和矩阵 import pandas as pd # 用于处理数据表格 import tensorflow as tf # TensorFlow库,用于构建和训练机器学习模型 from keras import layers # Keras库中的层模块 from keras.layers import StringLookup # Keras库中的字符串查找层模块 准备数据 下载并准备数据框

首先,让我们下载movielens数据。

下载的文件夹将包含三个数据文件:users.dat,movies.dat和ratings.dat。

# 导入必要的库 from urllib.request import urlretrieve from zipfile import ZipFile # 下载movielens数据集的zip文件 urlretrieve("http://files.grouplens.org/datasets/movielens/ml-1m.zip", "movielens.zip") # 创建一个ZipFile对象,用于解压缩zip文件 zip_file = ZipFile("movielens.zip", "r") # 解压缩zip文件中的所有内容到当前目录 zip_file.extractall()

然后,我们使用正确的列名将数据加载到pandas DataFrames中。

# 导入所需的库 import pandas as pd # 读取用户数据 users = pd.read_csv( "ml-1m/users.dat", # 用户数据文件路径 sep="::", # 分隔符为双冒号 names=["user_id", "sex", "age_group", "occupation", "zip_code"], # 列名 encoding="ISO-8859-1", # 使用ISO-8859-1编码 engine="python", # 使用Python解析引擎 ) # 读取评分数据 ratings = pd.read_csv( "ml-1m/ratings.dat", # 评分数据文件路径 sep="::", # 分隔符为双冒号 names=["user_id", "movie_id", "rating", "unix_timestamp"], # 列名 encoding="ISO-8859-1", # 使用ISO-8859-1编码 engine="python", # 使用Python解析引擎 ) # 读取电影数据 movies = pd.read_csv( "ml-1m/movies.dat", # 电影数据文件路径 sep="::", # 分隔符为双冒号 names=["movie_id", "title", "genres"], # 列名 encoding="ISO-8859-1", # 使用ISO-8859-1编码 engine="python", # 使用Python解析引擎 )

在这里,我们对列的数据类型进行一些简单的数据处理,以修复数据类型。

# 给用户数据添加user_id前缀 users["user_id"] = users["user_id"].apply(lambda x: f"user_{x}") # 给用户数据添加age_group前缀 users["age_group"] = users["age_group"].apply(lambda x: f"group_{x}") # 给用户数据添加occupation前缀 users["occupation"] = users["occupation"].apply(lambda x: f"occupation_{x}") # 给电影数据添加movie_id前缀 movies["movie_id"] = movies["movie_id"].apply(lambda x: f"movie_{x}") # 给评分数据添加movie_id前缀 ratings["movie_id"] = ratings["movie_id"].apply(lambda x: f"movie_{x}") # 给评分数据添加user_id前缀 ratings["user_id"] = ratings["user_id"].apply(lambda x: f"user_{x}") # 将评分数据中的rating转换为浮点型 ratings["rating"] = ratings["rating"].apply(lambda x: float(x))

每部电影都有多个类型。我们在movies数据框中将它们拆分为单独的列。

# 定义电影类型列表 genres = ["Action", "Adventure", "Animation", "Children's", "Comedy", "Crime"] genres += ["Documentary", "Drama", "Fantasy", "Film-Noir", "Horror", "Musical"] genres += ["Mystery", "Romance", "Sci-Fi", "Thriller", "War", "Western"] # 遍历电影类型列表 for genre in genres: # 对于每个电影类型,将movies["genres"]中的每个电影的类型字符串进行处理 # 使用lambda函数将字符串转换为对应的二进制值(1表示包含该类型,0表示不包含该类型) movies[genre] = movies["genres"].apply( lambda values: int(genre in values.split("|")) ) 将电影评分数据转换为序列

首先,让我们使用unix_timestamp对评分数据进行排序,然后按user_id对movie_id值和rating值进行分组。

输出的DataFrame将为每个user_id记录两个有序列表(按评分日期排序):他们评价过的电影和他们对这些电影的评分。

# 导入必要的库 import pandas as pd # 按照"unix_timestamp"列对"ratings"数据集进行排序,并按"user_id"分组 ratings_group = ratings.sort_values(by=["unix_timestamp"]).groupby("user_id") # 创建一个新的数据框ratings_data,包含以下列:user_id, movie_ids, ratings, timestamps ratings_data = pd.DataFrame( data={ "user_id": list(ratings_group.groups.keys()), # 获取分组后的用户ID "movie_ids": list(ratings_group.movie_id.apply(list)), # 获取每个用户对应的电影ID列表 "ratings": list(ratings_group.rating.apply(list)), # 获取每个用户对应的评分列表 "timestamps": list(ratings_group.unix_timestamp.apply(list)), # 获取每个用户对应的时间戳列表 } )

现在,让我们将movie_ids列表分割成一组固定长度的序列。 我们对ratings也做同样的操作。设置sequence_length变量来改变输入序列的长度。 您还可以更改step_size来控制为每个用户生成的序列数量。

# 定义窗口大小和步长 sequence_length = 4 step_size = 2 # 创建序列函数,输入值、窗口大小和步长,返回序列列表 def create_sequences(values, window_size, step_size): sequences = [] # 存储序列的列表 start_index = 0 # 起始索引 while True: end_index = start_index + window_size # 结束索引 seq = values[start_index:end_index] # 根据窗口大小切片得到序列 if len(seq) "user_id": list(users.user_id.unique()), # 用户ID特征对应的唯一值列表 "movie_id": list(movies.movie_id.unique()), # 电影ID特征对应的唯一值列表 "sex": list(users.sex.unique()), # 性别特征对应的唯一值列表 "age_group": list(users.age_group.unique()), # 年龄组特征对应的唯一值列表 "occupation": list(users.occupation.unique()), # 职业特征对应的唯一值列表 } # 定义USER_FEATURES为一个列表,包含了用户特征 USER_FEATURES = ["sex", "age_group", "occupation"] # 定义MOVIE_FEATURES为一个列表,包含了电影特征 MOVIE_FEATURES = ["genres"] 为训练和评估创建 tf.data.Dataset # 定义一个函数get_dataset_from_csv,用于从csv文件中获取数据集 # 参数: # - csv_file_path:csv文件的路径 # - shuffle:是否对数据进行洗牌,默认为False # - batch_size:批处理的大小,默认为128 def get_dataset_from_csv(csv_file_path, shuffle=False, batch_size=128): # 定义一个内部函数process,用于处理特征 # 参数: # - features:特征数据 def process(features): # 从特征中获取电影ID序列的字符串 movie_ids_string = features["sequence_movie_ids"] # 将电影ID序列字符串按逗号分割,并转换为张量 sequence_movie_ids = tf.strings.split(movie_ids_string, ",").to_tensor() # 序列中的最后一个电影ID是目标电影 features["target_movie_id"] = sequence_movie_ids[:, -1] # 将特征中的电影ID序列更新为除了最后一个电影ID之外的序列 features["sequence_movie_ids"] = sequence_movie_ids[:, :-1] # 从特征中获取评分序列的字符串 ratings_string = features["sequence_ratings"] # 将评分序列字符串按逗号分割,并转换为浮点数类型的张量 sequence_ratings = tf.strings.to_number( tf.strings.split(ratings_string, ","), tf.dtypes.float32 ).to_tensor() # 序列中的最后一个评分是模型要预测的目标 target = sequence_ratings[:, -1] # 将特征中的评分序列更新为除了最后一个评分之外的序列 features["sequence_ratings"] = sequence_ratings[:, :-1] return features, target # 使用tf.data.experimental.make_csv_dataset函数从csv文件中创建数据集 dataset = tf.data.experimental.make_csv_dataset( csv_file_path, batch_size=batch_size, column_names=CSV_HEADER, num_epochs=1, header=False, field_delim="|", shuffle=shuffle, ).map(process) return dataset 创建模型输入 # 定义一个函数create_model_inputs,用于创建模型的输入 def create_model_inputs(): # 返回一个字典,包含模型的输入 return { "user_id": keras.Input(name="user_id", shape=(1,), dtype="string"), # 用户ID,输入形状为(1,),数据类型为字符串 "sequence_movie_ids": keras.Input( name="sequence_movie_ids", shape=(sequence_length - 1,), dtype="string" ), # 电影序列ID,输入形状为(sequence_length - 1,),数据类型为字符串 "target_movie_id": keras.Input( name="target_movie_id", shape=(1,), dtype="string" ), # 目标电影ID,输入形状为(1,),数据类型为字符串 "sequence_ratings": keras.Input( name="sequence_ratings", shape=(sequence_length - 1,), dtype=tf.float32 ), # 电影评分序列,输入形状为(sequence_length - 1,),数据类型为浮点数 "sex": keras.Input(name="sex", shape=(1,), dtype="string"), # 性别,输入形状为(1,),数据类型为字符串 "age_group": keras.Input(name="age_group", shape=(1,), dtype="string"), # 年龄组,输入形状为(1,),数据类型为字符串 "occupation": keras.Input(name="occupation", shape=(1,), dtype="string"), # 职业,输入形状为(1,),数据类型为字符串 } 编码输入特征

encode_input_features 方法的工作原理如下:

使用 layers.Embedding 对每个分类用户特征进行编码,其中嵌入维度等于特征的词汇量的平方根。 这些特征的嵌入被连接起来形成一个单一的输入张量。

使用 layers.Embedding 对电影序列中的每个电影和目标电影进行编码,其中维度大小为电影数量的平方根。

对每个电影的多热流派向量与其嵌入向量进行连接,并使用非线性 layers.Dense 处理,输出相同电影嵌入维度的向量。

在序列中的每个电影嵌入中添加位置嵌入,然后乘以其来自评分序列的评分。

将目标电影嵌入连接到序列电影嵌入中,生成一个形状为 [batch size, sequence length, embedding size] 的张量,符合变压器架构的注意力层的预期形状。

该方法返回一个由两个元素组成的元组:encoded_transformer_features 和 encoded_other_features。

# 编码输入特征 ## 定义函数encode_input_features,用于将输入特征进行编码 ### 参数: - inputs:包含输入特征的字典 - include_user_id:是否包含用户ID,默认为True - include_user_features:是否包含用户特征,默认为True - include_movie_features:是否包含电影特征,默认为True ### 返回值: - encoded_transformer_features:编码后的转换器特征 - encoded_other_features:编码后的其他特征 ## 初始化编码后的转换器特征列表和其他特征列表 encoded_transformer_features = [] encoded_other_features = [] ## 初始化其他特征名称列表 other_feature_names = [] ## 如果include_user_id为True,则将"user_id"添加到其他特征名称列表中 if include_user_id: other_feature_names.append("user_id") ## 如果include_user_features为True,则将USER_FEATURES中的特征名称添加到其他特征名称列表中 if include_user_features: other_feature_names.extend(USER_FEATURES) ## 对用户特征进行编码 for feature_name in other_feature_names: # 将字符串输入值转换为整数索引 vocabulary = CATEGORICAL_FEATURES_WITH_VOCABULARY[feature_name] idx = StringLookup(vocabulary=vocabulary, mask_token=None, num_oov_indices=0)( inputs[feature_name] ) # 计算嵌入维度 embedding_dims = int(math.sqrt(len(vocabulary))) # 创建指定维度的嵌入层 embedding_encoder = layers.Embedding( input_dim=len(vocabulary), output_dim=embedding_dims, name=f"{feature_name}_embedding", ) # 将索引值转换为嵌入表示 encoded_other_features.append(embedding_encoder(idx)) ## 创建用户特征的单个嵌入向量 if len(encoded_other_features) > 1: encoded_other_features = layers.concatenate(encoded_other_features) elif len(encoded_other_features) == 1: encoded_other_features = encoded_other_features[0] else: encoded_other_features = None ## 创建电影嵌入编码器 movie_vocabulary = CATEGORICAL_FEATURES_WITH_VOCABULARY["movie_id"] movie_embedding_dims = int(math.sqrt(len(movie_vocabulary))) # 创建查找表,将字符串值转换为整数索引 movie_index_lookup = StringLookup( vocabulary=movie_vocabulary, mask_token=None, num_oov_indices=0, name="movie_index_lookup", ) # 创建指定维度的嵌入层 movie_embedding_encoder = layers.Embedding( input_dim=len(movie_vocabulary), output_dim=movie_embedding_dims, name=f"movie_embedding", ) # 创建电影类型的向量查找表 genre_vectors = movies[genres].to_numpy() movie_genres_lookup = layers.Embedding( input_dim=genre_vectors.shape[0], output_dim=genre_vectors.shape[1], embeddings_initializer=keras.initializers.Constant(genre_vectors), trainable=False, name="genres_vector", ) # 创建电影类型的处理层 movie_embedding_processor = layers.Dense( units=movie_embedding_dims, activation="relu", name="process_movie_embedding_with_genres", ) ## 定义一个函数,用于编码给定的电影ID def encode_movie(movie_id): # 将字符串输入值转换为整数索引 movie_idx = movie_index_lookup(movie_id) movie_embedding = movie_embedding_encoder(movie_idx) encoded_movie = movie_embedding if include_movie_features: movie_genres_vector = movie_genres_lookup(movie_idx) encoded_movie = movie_embedding_processor( layers.concatenate([movie_embedding, movie_genres_vector]) ) return encoded_movie ## 编码目标电影ID target_movie_id = inputs["target_movie_id"] encoded_target_movie = encode_movie(target_movie_id) ## 编码序列电影ID sequence_movies_ids = inputs["sequence_movie_ids"] encoded_sequence_movies = encode_movie(sequence_movies_ids) # 创建位置嵌入 position_embedding_encoder = layers.Embedding( input_dim=sequence_length, output_dim=movie_embedding_dims, name="position_embedding", ) positions = tf.range(start=0, limit=sequence_length - 1, delta=1) encodded_positions = position_embedding_encoder(positions) # 获取序列评分,将其合并到电影编码中 sequence_ratings = inputs["sequence_ratings"] sequence_ratings = keras.ops.expand_dims(sequence_ratings, -1) # 将位置编码添加到电影编码中,并乘以评分 encoded_sequence_movies_with_poistion_and_rating = layers.Multiply()( [(encoded_sequence_movies + encodded_positions), sequence_ratings] ) # 构建转换器的输入 for i in range(sequence_length - 1): feature = encoded_sequence_movies_with_poistion_and_rating[:, i, ...] feature = keras.ops.expand_dims(feature, 1) encoded_transformer_features.append(feature) encoded_transformer_features.append(encoded_target_movie) encoded_transformer_features = layers.concatenate( encoded_transformer_features, axis=1 ) return encoded_transformer_features, encoded_other_features 创建一个二叉搜索树模型 # 创建模型 ## 设置参数 include_user_id = False # 是否包含用户ID特征 include_user_features = False # 是否包含用户特征 include_movie_features = False # 是否包含电影特征 hidden_units = [256, 128] # 隐藏层单元数 dropout_rate = 0.1 # Dropout比例 num_heads = 3 # 多头注意力机制的头数 ## 创建模型函数 def create_model(): inputs = create_model_inputs() # 创建模型输入 transformer_features, other_features = encode_input_features( inputs, include_user_id, include_user_features, include_movie_features ) # 编码输入特征 # 创建多头注意力层 attention_output = layers.MultiHeadAttention( num_heads=num_heads, key_dim=transformer_features.shape[2], dropout=dropout_rate )(transformer_features, transformer_features) # Transformer块 attention_output = layers.Dropout(dropout_rate)(attention_output) x1 = layers.Add()([transformer_features, attention_output]) x1 = layers.LayerNormalization()(x1) x2 = layers.LeakyReLU()(x1) x2 = layers.Dense(units=x2.shape[-1])(x2) x2 = layers.Dropout(dropout_rate)(x2) transformer_features = layers.Add()([x1, x2]) transformer_features = layers.LayerNormalization()(transformer_features) features = layers.Flatten()(transformer_features) # 添加其他特征 if other_features is not None: features = layers.concatenate( [features, layers.Reshape([other_features.shape[-1]])(other_features)] ) # 全连接层 for num_units in hidden_units: features = layers.Dense(num_units)(features) features = layers.BatchNormalization()(features) features = layers.LeakyReLU()(features) features = layers.Dropout(dropout_rate)(features) outputs = layers.Dense(units=1)(features) # 输出层 model = keras.Model(inputs=inputs, outputs=outputs) # 创建模型 return model model = create_model() # 创建模型 运行训练和评估实验 # 编译模型 model.compile( optimizer=keras.optimizers.Adagrad(learning_rate=0.01), # 使用Adagrad优化器,学习率为0.01 loss=keras.losses.MeanSquaredError(), # 使用均方误差作为损失函数 metrics=[keras.metrics.MeanAbsoluteError()], # 使用平均绝对误差作为评估指标 ) # 读取训练数据 train_dataset = get_dataset_from_csv("train_data.csv", shuffle=True, batch_size=265) # 使用训练数据拟合模型 model.fit(train_dataset, epochs=5) # 读取测试数据 test_dataset = get_dataset_from_csv("test_data.csv", batch_size=265) # 在测试数据上评估模型 _, rmse = model.evaluate(test_dataset, verbose=0) print(f"Test MAE: {round(rmse, 3)}") # 打印测试数据上的平均绝对误差

你应该在测试数据上达到或接近0.7的平均绝对误差(MAE)。

结论

BST模型在其架构中使用Transformer层来捕捉推荐中用户行为序列的顺序信号。

您可以尝试使用不同的配置来训练该模型,例如增加输入序列长度并将模型训练更多个周期。此外,您还可以尝试包括其他特征,如电影发布年份和客户邮编,以及包括性别X类型等交叉特征。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3