記事ディレクトリ
説明: Behavior Sequence Transformer (BST) モデルを使用した Movielens の評価予測。
導入
この例では、Movielens データセットを使用した Qiwei Chen らによる Behavioral Sequence Transformer (BST) モデルの使用を示します。BST モデルは、映画を視聴して評価するときのユーザーの一連の行動、ユーザー プロフィールや映画の特徴を利用して、対象の映画に対するユーザーの評価を予測します。
より具体的には、BST モデルは、次の入力を受け入れることによって対象映画の評価を予測するように設計されています。
- ユーザーが視聴した、 を含む映画の固定長シーケンス
movie_ids
。 - ユーザーが視聴した、映画を含む固定長の映画シーケンス
ratings
。 - 、、およびを含むユーザー特性のコレクション。
user_id
sex
occupation
age_group
- 入力シーケンスと各ムービーのターゲット ムービー
genres
のコレクション。 - 評価を予測します
target_movie_id
。
この例では、元の BST モデルに次の変更を加えます。
- 映画の特徴 (ジャンル) をトランス層の外側の「その他の特徴」として扱うのではなく、各入力シーケンスとターゲット映画の埋め込みプロセスに組み込みます。
- 入力シーケンス内の映画の評価とシーケンス内での位置を利用して、セルフアテンション層にフィードする前にそれらを更新します。
この例は TensorFlow 2.4 以降で実行する必要があることに注意してください。
データセット
Movielens データセットの 1M バージョンを使用します。
このデータセットには、4,000 本の映画に対する 6,000 人のユーザーからの約 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のデータをダウンロードしましょう。
ダウンロードしたフォルダーには、 と の 3 つのデータ ファイルが含ま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 DataFrame にロードします。
# 导入所需的库
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
グループ化しましょう。
出力データフレームには、user_id
レコードごとに 2 つの順序付きリスト (評価日でソート)、つまりレビューした映画とその映画に与えた評価が含まれます。
# 导入必要的库
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) < window_size: # 如果序列长度小于窗口大小
seq = values[-window_size:] # 则取最后窗口大小长度的序列
if len(seq) == window_size: # 如果序列长度等于窗口大小
sequences.append(seq) # 将序列添加到列表中
break # 结束循环
sequences.append(seq) # 将序列添加到列表中
start_index += step_size # 更新起始索引
return sequences # 返回序列列表
# 对电影ID列应用create_sequences函数,将结果赋值给movie_ids列
ratings_data.movie_ids = ratings_data.movie_ids.apply(
lambda ids: create_sequences(ids, sequence_length, step_size)
)
# 对评分列应用create_sequences函数,将结果赋值给ratings列
ratings_data.ratings = ratings_data.ratings.apply(
lambda ids: create_sequences(ids, sequence_length, step_size)
)
# 删除timestamps列
del ratings_data["timestamps"]
その後、各シリーズが DataFrame 内の個別のレコードになるように出力を処理します。さらに、ユーザーの特性と評価データを結び付けます。
# 导入所需的库
import pandas as pd
# 将ratings_data中的"movie_ids"列拆分成多行,每行只包含一个电影ID,并重置索引
ratings_data_movies = ratings_data[["user_id", "movie_ids"]].explode("movie_ids", ignore_index=True)
# 将ratings_data中的"ratings"列拆分成多行,每行只包含一个评分,并重置索引
ratings_data_rating = ratings_data[["ratings"]].explode("ratings", ignore_index=True)
# 将拆分后的"movie_ids"和"ratings"两列合并为一个DataFrame
ratings_data_transformed = pd.concat([ratings_data_movies, ratings_data_rating], axis=1)
# 根据"user_id"列将ratings_data_transformed与users进行连接
ratings_data_transformed = ratings_data_transformed.join(users.set_index("user_id"), on="user_id")
# 将"movie_ids"列中的每个元素转换为字符串,并用逗号分隔
ratings_data_transformed.movie_ids = ratings_data_transformed.movie_ids.apply(lambda x: ",".join(x))
# 将"ratings"列中的每个元素转换为字符串,并用逗号分隔
ratings_data_transformed.ratings = ratings_data_transformed.ratings.apply(lambda x: ",".join([str(v) for v in x]))
# 删除ratings_data_transformed中的"zip_code"列
del ratings_data_transformed["zip_code"]
# 将列名"movie_ids"改为"sequence_movie_ids",将列名"ratings"改为"sequence_ratings"
ratings_data_transformed.rename(columns={
"movie_ids": "sequence_movie_ids", "ratings": "sequence_ratings"}, inplace=True)
sequence_length
4 と2を使用すると、step_size
最終的に 498,623 のシーケンスになります。
最後に、データをトレーニング セットとテスト セットに分割し、それぞれ合計データの 85% と 15% を占め、CSV ファイルとして保存します。
import numpy as np
# 生成一个与ratings_data_transformed.index长度相同的随机数数组,每个元素都是0到1之间的随机数
random_selection = np.random.rand(len(ratings_data_transformed.index)) <= 0.85
# 根据随机数数组,选择85%的数据作为训练数据
train_data = ratings_data_transformed[random_selection]
# 根据随机数数组,选择15%的数据作为测试数据
test_data = ratings_data_transformed[~random_selection]
# 将训练数据保存为CSV文件,不包含索引列,使用竖线作为分隔符,不包含表头
train_data.to_csv("train_data.csv", index=False, sep="|", header=False)
# 将测试数据保存为CSV文件,不包含索引列,使用竖线作为分隔符,不包含表头
test_data.to_csv("test_data.csv", index=False, sep="|", header=False)
メタデータの定義
# 定义CSV_HEADER为ratings_data_transformed的列名列表
CSV_HEADER = list(ratings_data_transformed.columns)
# 定义CATEGORICAL_FEATURES_WITH_VOCABULARY为一个字典,包含了几个特征及其对应的唯一值列表
CATEGORICAL_FEATURES_WITH_VOCABULARY = {
"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]
Transformer アーキテクチャのアテンション層の予想される形状に一致する、shape のテンソルが生成されます。 -
encoded_transformer_features
このメソッドは、と の2 つの要素で構成されるタプルを返します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)}") # 打印测试数据上的平均绝对误差
テスト データの平均絶対誤差 (MAE) が 0.7 に達するか、それに近い値になるはずです。
結論は
BST モデルは、そのアーキテクチャで Transformer レイヤーを使用して、レコメンデーション内のユーザー行動シーケンスの連続信号をキャプチャします。
入力シーケンスの長さを増やし、より多くのエポックに対してモデルをトレーニングするなど、さまざまな構成を試してモデルをトレーニングできます。さらに、映画の公開年や顧客の郵便番号などの他の機能を含めたり、性別 X タイプなどの交差機能を含めたりすることもできます。