ML&DL/Recommender System

[Recommender System] implicit repository와 직접 구현한 als 결과 비교

거북이주인장 2024. 9. 21. 17:46

추천 시스템의 알고리즘을 pytorch 또는 numpy을 사용하여 구현하는 프로젝트를 진행하고 있다. (PR은 언제든 환영!)

https://github.com/bohyunshin/recommender

 

GitHub - bohyunshin/recommender: Implementation of various recommender algorithm

Implementation of various recommender algorithm. Contribute to bohyunshin/recommender development by creating an account on GitHub.

github.com

 

그 중 첫 단계로 implicit feedback을 모델링하는 weighted matrix factorization을 als로 구현했는데 implicit repository와 학습 결과를 비교해보고 느낀점을 기록하고자 한다.

 

결론부터 얘기하면 아래와 같다.

  • 유저, 아이템 임베딩의 초기값은 다르게 시작했고 이터레이션을 돌면서 다른 임베딩 값으로 수렴하는데, 추천 결과를 평가하는 메트릭 측면에서는 거의 유사한 성능을 보였다.
  • implicit 추천 모델은 validation set에 대한 argument가 없다.. 그래서 따로 패키지를 살짝 수정했다.
  • implicit 추천 모델은 cython을 사용하기 때문에 직접 구현한 모델에 비해서 월등히 빠르다.

이제 implicit의 als 결과와 직접 구현한 als 결과를 차례로 코드를 통해 살펴보자. (직접 구현했다기보다.. implicit repository에 cython을 사용하지 않은 구현체를 적절히 조합한 것이긴 하다 ㅎㅎ)

ALS from implicit repository

필요한 함수 및 클래스를 로드하고 movielens 데이터를 train / validation set으로 나눠준다.

 

import implicit
from implicit.als import AlternatingLeastSquares
from implicit.datasets.movielens import get_movielens
from implicit.nearest_neighbours import bm25_weight
from implicit.evaluation import train_test_split, ranking_metrics_at_k

import numpy as np

import logging
logging.basicConfig(level=logging.DEBUG)


titles, ratings = get_movielens("1m")
min_rating = 4

# remove things < min_rating, and convert to implicit dataset
# by considering ratings as a binary preference only
ratings.data[ratings.data < min_rating] = 0
ratings.eliminate_zeros()
ratings.data = np.ones(len(ratings.data))

# lets weight these models by bm25weight.
ratings = (bm25_weight(ratings, B=0.9) * 5).tocsr()
user_ratings = ratings.T.tocsr()

train, validation = train_test_split(user_ratings)

train.shape, validation.shape

# ((6041, 3953), (6041, 3953))

 

데이터를 불러오는 과정은 implicit 공식 문서를 참고했다. logging의 level을 debug로 바꿔줘서 찍히는 로그를 전부 보고자 한다.

 

여기서 몇가지 수정을 해야한다. 먼저, implicit의 als.fit 메써드를 살펴보면 validation user_items 데이터를 받는 부분이 없다. 그에 따라서 iteration을 돌면서 training loss만 계산할 수 있다. 솔직히 이 부분은 아직도 의아하다. validation loss을 당연히 봐야하는거 아닌가? training loss는 최소화하는 방향으로 학습이 당연히 진행되는데 validation loss도 떨어지는지 확인해야하는거 아닌가? 라는 의문이 들었다. (PR을 올리든가 해야지..) 어찌됐든 현재 코드 기준으로 iteration별 validation loss을 계산하기 위해서는 살짝 수정을 해야하는 것은 사실이다.

 

먼저 `fit` 메써드를 `_fit`으로 이름을 바꾸고 `user_items_val`을 argument로 받도록 수정하자.

 

def _fit(self, user_items, user_items_val, show_progress=True, callback=None):
	pass

 

`fit`을 유지하지 않는 이유는, MatrixFactorizationBase을 상속받고 `fit`이 abstract method로 정의되기 때문에 argument을 추가하려면 머리가 복잡해지기 때문이다. `_fit` 메써드 내부에서 validation loss을 계산하고 이를 로그로 찍는 부분을 추가하자.

 

def _fit(self, user_items, user_items_val, show_progress=True, callback=None):
    ...
    with tqdm(total=self.iterations, disable=not show_progress) as progress:
        # alternate between learning the user_factors from the item_factors and vice-versa
        for iteration in range(self.iterations):
            s = time.time()
            solver(
                Cui,
                self.user_factors,
                self.item_factors,
                self.regularization,
                num_threads=self.num_threads,
            )
            solver(
                Ciu,
                self.item_factors,
                self.user_factors,
                self.regularization,
                num_threads=self.num_threads,
            )
            progress.update(1)

            if self.calculate_training_loss:
                loss = _als.calculate_loss(
                    Cui,
                    self.user_factors,
                    self.item_factors,
                    self.regularization,
                    num_threads=self.num_threads,
                )
                # add this line
                loss_val = _als.calculate_loss(
                    user_items_val, 
                    self.user_factors, 
                    self.item_factors, 
                    self.regularization, 
                    num_threads=self.num_threads,
                )
                progress.set_postfix({"loss": loss})

                if not show_progress:
                    log.info("loss %.4f", loss)
                # add this line
                log.info(f"{iteration} iteration")
                log.info(f"training loss {loss}")
                log.info(f"validation loss {loss_val}\n\n")

 

`add this line`이라고 되어 있는 부분을 추가하면 된다. 중요한 점은 기존의 `fit` 메써드를 빈 함수로 따로 만들어야 한다는 것이다.

 

def fit(self, user_items, show_progress=True, callback=None):
    print("hi")

 

그래야 abstract method에서 오류가 발생하지 않는다.

 

이제 `_fit` 메써드를 실행해보자.

 

# initialize a model
model = implicit.als.AlternatingLeastSquares(factors=50, calculate_training_loss=True)

# train the model on a sparse matrix of user/item/confidence weights
model._fit(train, validation)

metrics = ranking_metrics_at_k(model, train, validation, K=50)

 

그러면 아래와 같은 로그를 확인할 수 있다. 초기화 되는 유저, 아이템 임베딩이 다르기 때문에 loss 값은 다를 수 있다. 중요한 점은 validation loss가 iteration이 지남에 따라 감소하고 있다는 점이다.

더보기

INFO:implicit:0 iteration
INFO:implicit:training loss 0.07680225227734423
INFO:implicit:validation loss 0.07053308448967677


INFO:implicit:1 iteration
INFO:implicit:training loss 0.0589116823289642
INFO:implicit:validation loss 0.06377584152606569


INFO:implicit:2 iteration
INFO:implicit:training loss 0.05458549301044711
INFO:implicit:validation loss 0.061474110253995705


INFO:implicit:3 iteration
INFO:implicit:training loss 0.05277066631183916
INFO:implicit:validation loss 0.06044628944619289


INFO:implicit:4 iteration
INFO:implicit:training loss 0.05177694675667218
INFO:implicit:validation loss 0.05985852763716888


INFO:implicit:5 iteration
INFO:implicit:training loss 0.051143414399963665
INFO:implicit:validation loss 0.05945100571435003


INFO:implicit:6 iteration
INFO:implicit:training loss 0.05069987167540387
INFO:implicit:validation loss 0.05914462174749819


INFO:implicit:7 iteration
INFO:implicit:training loss 0.050370101776719495
INFO:implicit:validation loss 0.05890415313016098


INFO:implicit:8 iteration
INFO:implicit:training loss 0.05011428081499358
INFO:implicit:validation loss 0.058709795060865176


INFO:implicit:9 iteration
INFO:implicit:training loss 0.04990923586003092
INFO:implicit:validation loss 0.05854900858743994


INFO:implicit:10 iteration
INFO:implicit:training loss 0.04974056202665798
INFO:implicit:validation loss 0.0584133894834947


INFO:implicit:11 iteration
INFO:implicit:training loss 0.04959886574873753
INFO:implicit:validation loss 0.05829721002184366


INFO:implicit:12 iteration
INFO:implicit:training loss 0.049477850776689175
INFO:implicit:validation loss 0.05819659116830305


INFO:implicit:13 iteration
INFO:implicit:training loss 0.04937306923146867
INFO:implicit:validation loss 0.05810860465341211


INFO:implicit:14 iteration
INFO:implicit:training loss 0.04928132508512776
INFO:implicit:validation loss 0.05803105778093609

 

`metrics`을 확인해보면 아래와 같다.

{'precision': 0.3082614176282362,
 'map': 0.10125272640145089,
 'ndcg': 0.2595410235007538,
 'auc': 0.6718663630485887}

 

이제 직접 구현한 als에서도 validation loss가 줄어드는지 확인하고 metrics가 유사한지 확인해보자.

ALS from own implementation

먼저 아래 레포를 클론 땡겨오자.

https://github.com/bohyunshin/recommender

 

GitHub - bohyunshin/recommender: Implementation of various recommender algorithm

Implementation of various recommender algorithm. Contribute to bohyunshin/recommender development by creating an account on GitHub.

github.com

 

그러고 사용할 클래스 및 함수를 불러오자.

 

import sys
sys.path.append("/Users/user/personal/recommender/recommender")

from model.mf.implicit_mf import AlternatingLeastSquares as AlternatingLeastSquares_
from tools.evaluation import ranking_metrics_at_k as ranking_metrics_at_k_

 

implicit에서 사용하는 클래스 및 함수와 구분하기 위해서 `_`을 덧붙였다. 위에서 구분한 train, validation 데이터를 사용하여 학습을 진행해보자. factors, iterations은 동일하게 맞춰준다.

 

als_ = AlternatingLeastSquares_(factors=50, iterations=15)
als_.fit(user_items=train, val_user_items=validation)

 

그러면 아래와 같은 로그를 확인할 수 있다.

더보기

############ iteration: 0 ############
training loss: 0.044355621756434614
validation loss: 0.029653280021168796


############ iteration: 1 ############
training loss: 0.02872974798248266
validation loss: 0.023796389876928673


############ iteration: 2 ############
training loss: 0.02536629914855224
validation loss: 0.02219742972559087


############ iteration: 3 ############
training loss: 0.024113676420708257
validation loss: 0.021510708202433306


############ iteration: 4 ############
training loss: 0.02342402416887215
validation loss: 0.021081975258720048


############ iteration: 5 ############
training loss: 0.022968476978557497
validation loss: 0.02077087827487173


############ iteration: 6 ############
training loss: 0.022635921743018882
validation loss: 0.020527492440306484


############ iteration: 7 ############
training loss: 0.022378388362347238
validation loss: 0.02032898682034748


############ iteration: 8 ############
training loss: 0.02217140145621233
validation loss: 0.02016317449815807


############ iteration: 9 ############
training loss: 0.02200072565282198
validation loss: 0.02002254685257935


############ iteration: 10 ############
training loss: 0.02185724178595146
validation loss: 0.01990192252510704


############ iteration: 11 ############
training loss: 0.021734741926726538
validation loss: 0.019797474433910958


############ iteration: 12 ############
training loss: 0.021628792887953538
validation loss: 0.0197062425132298


############ iteration: 13 ############
training loss: 0.0215360927748088
validation loss: 0.019625881733354746


############ iteration: 14 ############
training loss: 0.021454167246968827
validation loss: 0.019554514366390136

validation loss가 감소하고 있음을 확인할 수 있다. 절대적인 loss의 값은 다르긴한데.. 아마도 학습되는 유저 / 아이템 임베딩이 달라서 그런거 아닐까. metrics 값을 확인해보자.

 

metrics_ = ranking_metrics_at_k_(als_, train, validation, K=50)
metrics_

"""
{'precision': 0.3087171531077281,
 'map': 0.1010581148645734,
 'ndcg': np.float64(0.25971098411937965),
 'auc': 0.6722618029028539}
"""

 

implicit의 als 적합 결과와 거의 비슷함을 확인할 수 있다. (휴 드디어 결과 확인 완료)

Conclusion

implicit의 als 결과와 직접 구현한 als의 결과를 비교해봄으로써 직접 구현한 als 결과에 더욱 신뢰를 가질 수 있게 되었다. 시간이 된다면 implicit 코드에서 validation loss을 구하는 로직을 한번 구현해봐야겠다. 또한, cython의 중요성도 몸소 체험할 수 있었다. 그렇게 어려워보이지는 않은데.. 한번 날잡고 공부해서 직접 구현한 코드의 효율성을 높여봐야겠다.