* 이 글은 다음의 문서를 읽고 한번 더 해석을 해본 글 입니다.
* tensorflow recommenders 를 TFRS라고 하겠습니다.
https://www.tensorflow.org/recommenders/examples/listwise_ranking?hl=ko
TFRS 에서의 pointwise, pairwise, listwise 구현
1. 학습데이터 준비
listwise의 경우 하나의 학습 데이터 마다 여러 아이템간 상관관계를 고려해야 하기 때문에, 일반적인 사용자 - 아이템 상호작용 데이터를 사용하지 못합니다.
아래 코드 블럭은 pointwise 학습 정보를 listwise로 바꾸는 예시 입니다.
[{ 사용자 X, 영화 Y, 사용자 X의 영화 Y 에 대한 점수}, ...]
-> [ {사용자 X, 영화 Y_1, Y_2, ... , 사용자 X의 영화 Y_1 에 대한 점수, Y_2에 대한 점수, ...}, ... )
print(train.take(1).element_spec)
"""
{'movie_title': <tf.Tensor: shape=(), dtype=string, numpy=b'Postman, The (1997)'>,
'user_id': <tf.Tensor: shape=(), dtype=string, numpy=b'681'>,
'user_rating': <tf.Tensor: shape=(), dtype=float32, numpy=4.0>}
"""
train = tfrs.examples.movielens.sample_listwise(
train,
num_list_per_user=50,
num_examples_per_list=5,
seed=42
)
print(train.take(1).element_spec)
"""
{'movie_title': <tf.Tensor: shape=(5,), dtype=string, numpy=
array([b'Postman, The (1997)', b'Liar Liar (1997)', b'Contact (1997)',
b'Welcome To Sarajevo (1997)',
b'I Know What You Did Last Summer (1997)'], dtype=object)>,
'user_id': <tf.Tensor: shape=(), dtype=string, numpy=b'681'>,
'user_rating': <tf.Tensor: shape=(5,), dtype=float32, numpy=array([4., 5., 1., 4., 1.], dtype=float32)>}
"""
- 한 사용자 마다 50개의 리스트가 생성이 되며, 각 리스트에는 5개의 영화와 영화에 대한 점수가 있습니다.
2. 투타워 모델 생성
class RankingModel(tfrs.Model):
def __init__(self, loss):
super().__init__()
embedding_dimension = 32
self.user_embeddings = tf.keras.Sequential([
tf.keras.layers.StringLookup(
vocabulary=unique_user_ids),
tf.keras.layers.Embedding(len(unique_user_ids) + 2, embedding_dimension)
], name="user_tower")
self.movie_embeddings = tf.keras.Sequential([
tf.keras.layers.StringLookup(
vocabulary=unique_movie_titles),
tf.keras.layers.Embedding(len(unique_movie_titles) + 2, embedding_dimension)
], name="item_tower")
self.score_model = tf.keras.Sequential([
tf.keras.layers.Dense(256, activation="relu"),
tf.keras.layers.Dense(64, activation="relu"),
tf.keras.layers.Dense(1)
], name="score_tower")
self.task = tfrs.tasks.Ranking(
loss=loss,
metrics=[
tfr.keras.metrics.NDCGMetric(name="ndcg_metric"),
tf.keras.metrics.RootMeanSquaredError()
]
)
def call(self, features):
user_embeddings = self.user_embeddings(features["user_id"])
movie_embeddings = self.movie_embeddings(features["movie_title"])
# 한 번의 추론 과정에서 추론을 할 영화의 수, 예제에서는 5
list_length = features["movie_title"].shape[1]
# 한명의 사용자 정보와, 여러 영화의 정보를 비교 할 수 있도록 복사
# (None, 32) -> (None, 1, 32) -> (None, 5, 32)
user_embedding_repeated = tf.repeat(
tf.expand_dims(user_embeddings, 1), [list_length], axis=1,
name="user_embedding_repeated"
)
concatenated_embeddings = tf.concat(
[user_embedding_repeated, movie_embeddings], 2,
name="concatenated"
)
return self.score_model(concatenated_embeddings)
def compute_loss(self, features, training=False):
labels = features.pop("user_rating")
scores = self(features)
return self.task(
labels=labels,
predictions=tf.squeeze(scores, axis=-1),
)
클래스 파라미터로 loss를 받아 오는 구조로 되어 있어, 여러 loss 대해 다른 모델을 만들고 비교 할 예정입니다.
pointwise two tower 모델에 한 사용자에 대해 여러 아이템을 각각 확인 할 수 있도록 call 함수 부분을 수정한 것을 볼 수 있습니다.
모델의 구조를 그려보면 다음과 같습니다.
3. 다른 손실 함수를 사용한 모델과 비고.
pointwise, pairwise, listwise에 따라 다른 손실함수를 사용한 모델을 만들고, 비교 하였습니다.
MeanSquared
# loss = mean(square(y_true - y_pred))
mse_model = RankingModel(tf.keras.losses.MeanSquaredError())
mse_model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))
mse_model.fit(cached_train, epochs=epochs, verbose=False)
하나의 데이터에서, 각 레이블 값과, 모델이 예측한 레이블의 차이를 제곱한 값의 평균을 손실 함수로 사용합니다.
PairwiseHingeLoss
# loss = sum_i sum_j I[y_i > y_j] * max(0, 1 - (s_i - s_j))
hinge_model = RankingModel(tfr.keras.losses.PairwiseHingeLoss())
hinge_model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))
hinge_model.fit(cached_train, epochs=epochs, verbose=False)
y는 실제 레이블 값을 의미하고, s는 모델이 예측한 값을 의미합니다.
I 는 인디케이터 함수로 [] 내용이 True면 1, 그렇지 않으면 0 입니다.
위 수식은 하나의 추론 데이터셋에 있는 아이템 쌍을 비교하면서
, 앞에 오는 아이템을 뒤에 오는 아이템보다 더 좋아했을때 I[ y_i > y_j] *
, 모델의 예측 결과로 뒤에 오는 아이템에 더 높은 점수를 준 경우 -( s_i - s_j) loss 를 생성시켜 줍니다.
아래 데이터가 있다고 가정 해 보겠습니다.
y_true = [[1., 0.]]
y_pred = [[0.6, 0.8]]
1) i가 0, j 가 1인 경우, y_i는 1, y_j는 0이 되어 y_i > y_j 는 True가 되며, I[ y_i > y_j]는 1이 되어 손실 계산에 적용 됩니다.
이는 첫번째 아이템인 i를 두 번째 아이템인 j보다 더 좋아하는것을 의미합니다.
2) (s_i - s_j) 는 첫번째 아이템과 두 번째 아이템의 점수차를 의미합니다.
위 예제에서는 -0.2가 됩니다.
즉 모델은 0.2 만큼 두 번째 모델을 선호할 것으로 예측하였습니다.
3) max(0, 1 - (s_i - s_j)) 를 계산하면 1.2 가 되어, 1.2만큼 손실이 발생한 것이 됩니다.
실제 학습데이터는 앞에 있는 아이템을 뒤에 있는 아이템보다 더 좋아했지만
, 모델의 뒤에 있는 아이템을 더 좋아 한 것으로 예측하여 손실이 발생한 것을 알 수 있습니다.
ListMLELoss
# loss = - log P(permutation_y | s)
# P(permutation_y | s) = Plackett-Luce probability of permutation_y given s
# permutation_y = permutation of items sorted by labels y.
listwise_model = RankingModel(tfr.keras.losses.ListMLELoss())
listwise_model.compile(optimizer=tf.keras.optimizers.Adagrad(0.1))
listwise_model.fit(cached_train, epochs=epochs, verbose=False)
1) permutation_y 는 y 레이블에 의해 정렬된 아이템의 목록을 의미합니다.
2) P(permutation_y | s) 는 예측값 s가 주어졌을 때, permutation_y 이 나올 Plackett-Luce 확률을 의미합니다.
Plackett-Luce확률은 주어진 점수에서, 특정 순열이 나올 확률을 계산하는 모델입니다.
3) Plackett-Luce 확률 모델은 다음과 같이 순서를 계산합니다.
Plackett-Luce 모델에서는 아이템을 하나씩 선택합니다.
각 선택 단계에서, 아이템을 선택할 확률은 주어진 가중치에 비례합니다.
남은 아이템의 가중치를 비교하며, 모든 아이템을 선택합니다.
이 과정을 수식으로 표현하면 다음과 같습니다.
π 는 순열을 의미하고, s_π_i 는 순열의 i번째 아이템의 가중치를 의미합니다.
4) 마지막으로 나올 확률에 로그를 취하고, -1을 곱하면 listwise loss 함수가 됩니다.
아래 데이터가 있다고 가정 해 보겠습니다.
y_true = [[1., 0.]]
y_pred = [[0.6, 0.8]]
1. permutation_y 는 [0, 1] 이 됩니다.
0번 아이템과, 1번 아이템의 스코어가 각각 0.6, 0.8인 경우 첫번째로 0번 아이템을 선택할 확률은
(0.6) / (0,6 + 0.8) -> 약 0.4286 입니다.
2. 해당 값에 로그와 음수를 순서대로 취합니다.
-ln(0.4286) -> 0.8473 이 됩니다.
3. Plackett-Luce에서는 해당 점수를 바로 사용하는것이 아닌, 지수함수를 적용하여 사용합니다. (e^s)
- 지수 함수를 사용하여 음수 스코어가 나온 경우도 확률로 표현 할 수 있습니다.
모델 별 성능 비교
NDCG 메트릭을 사용하여 각각 다른 Loss 함수를 사용한 모델의 학습 결과를 비교하였습니다.
(예제와는 다르게) listwise 모델은 pointwise 모델보다 성능이 낮아졌지만, pairwise 모델의 경우 pointwise 모델보다 더 높은 성능을 나타낸것을 확인 할 수 있습니다.
한계 및 생각해볼 점
한 번의 추론에 n개의 고정된 아이템만 고려하게 됩니다.
- n개보다 작은 아이탬에 대해 추론이 필요한 경우, 패딩을 통해 n개를 맞출 필요가 있습니다.
- n개보다 더 많은 아이템에 대해 추론이 필요한 경우, 검색 과정을 거쳐 top n개를 정한 후에 추론을 할 수 있습니다.
Pointwise, Pairwise, Listwise 과정이 loss 함수에서 이루어지며, 모델의 추론 과정 자체는 Pointwise로 이루어 지게 됩니다.
- (주어진 데이터 기준으로) pointwise loss 함수 대비 pairwise loss 함수의 성능이 더 높은것을 확인 했습니다.
- 그러나 모델의 추론 과정에서도 여러 아이템을 고려하도록 수정하면 더 높은 성능을 기대할 수 있습니다. (해보지 않음)
'Machine Learning > 추천시스템' 카테고리의 다른 글
Self-Attentive Sequential Recommendation 논문 리뷰 (3) | 2024.12.16 |
---|---|
Tensorflow recommenders 튜토리얼 후기 (2) | 2024.09.10 |
추천 시스템 캐글 대회 후기 (OTTO – Multi-Objective Recommender System) (0) | 2023.02.06 |
Deep Learning Recommendation Model for Personalization and Recommendation Systems 리뷰 (0) | 2022.08.22 |