본문 바로가기

Machine Learning/추천시스템

tensorflow recommenders는 어떻게 listwise ranking을 구현했을까?

* 이 글은 다음의 문서를 읽고 한번 더 해석을 해본 글 입니다.

* tensorflow recommenders 를 TFRS라고 하겠습니다.

https://www.tensorflow.org/recommenders/examples/listwise_ranking?hl=ko

 

목록별 순위  |  TensorFlow Recommenders

이 페이지는 Cloud Translation API를 통해 번역되었습니다. 목록별 순위 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 에서 기본 순위 튜토리얼 , 우리는 사용자

www.tensorflow.org

 

 

 

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 함수의 성능이 더 높은것을 확인 했습니다.

- 그러나 모델의 추론 과정에서도 여러 아이템을 고려하도록 수정하면 더 높은 성능을 기대할 수 있습니다. (해보지 않음)