본문 바로가기
ML | DL

ALS 추천시스템(Implicit 라이브러리)

by 편의점소세지 2021. 6. 1.
반응형

제가 얼마전에 추천시스템 만드는 과제를 수행하면서 공부했던 ALS 모델의 라이브러리 Implicit의 소스코드를 보면서 소개해 보겠습니다.

* 받은 과제는 고객의 클릭로그 기반 데이터로 협업필터링 모델을 작성하는 것이었습니다.

 

 ALS모델을 처음 공부한건 T-아카데미의 유튜브 영상이었습니다. 

https://www.youtube.com/watch?v=TFbTU9VG3is&t=1848s 

위 영상을 보시면 SGD가 무엇이고, ALS가 무엇인지 쉽게 알 수 있습니다.

Implicit 라이브러리의 소스코드를 좀 더 자세히 분해해서 어떤 식으로 모델이 학습되고 어떤식으로 추천을하는지 공유하겠습니다. 소스코드의 깃허브는 https://github.com/benfred/implicit 에 있습니다.

from implicit.als import AlternatingLeastSquares as ALS

als_model = ALS(factors=20, regularization=0.08, iterations = 20)
als_model.fit(click_sparse.T)

위 코드를 통해서 ALS 모델을 import 하고 AlternatingLeastSquares 클래스를 생성한뒤, fit함수로 모델을 학습하는 과정을 보겠습니다.

 

AlternatingLeastSquares 함수 -> implicit.cpu(gpu).als.AlternatingLeastSquares 클래스

 

AlternatingLeastSquares 클래스는 MatrixFactorizationBase를 상속받고,

MatrixFactorizationBase은 fit, recommend, rank_item 함수를 갖고 있는 RecommderBase 추상 클래스를 상속받습니다.

 

AlternatingLeastSquaresfit로 학습을 진행하는 과정을 보겠습니다. 

def fit(self, item_users, show_progress=True):
        random_state = check_random_state(self.random_state)

        Ciu = item_users
        
        if Ciu.dtype != np.float32:
            Ciu = Ciu.astype(np.float32)

        s = time.time()
        Cui = Ciu.T.tocsr()

        items, users = Ciu.shape

        s = time.time()

		if self.user_factors is None:
            self.user_factors = random_state.rand(users, self.factors).astype(self.dtype) * 0.01
        if self.item_factors is None:
            self.item_factors = random_state.rand(items, self.factors).astype(self.dtype) * 0.01
            
        self._item_norms = self._user_norms = None
        self._YtY = None
        self._XtX = None
        
        solver = self.solver

여기까지의 코드를 보겠습니다.

  • item_users라는 sparse matrix를 받아서 Ciu라는 변수명으로 선언하고, 이의 전치행렬을 Cui로 선언합니다.
  • 각각의 행과 열의 갯수를 itemsusers라는 변수로 저장해 놓습니다.
  • Class 선언시 초기값은 None이기 때문에, user_factorsitem_factors의 초기값을 (user, factor), (items, factor) 크기의 랜덤 matrix를 만듭니다.
    • factor의 default 값은 100이지만 일반적으로는 20으로 설정합니다. 
  • item_norms, user_norms, YtY, XtX 변수를 선언하고, class에 있는 solver 함수를 불러옵니다.
with tqdm(total=self.iterations, disable=not show_progress) as progress:

    for iteration in range(self.iterations):
          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,
              )
          progress.set_postfix({"loss": loss})
  • solver 함수를 이용해서 item_factoruser_factor값을 한번씩 고정해서 최적화를 합니다.
  • 이를 tqdm 라이브러리를 이용해서 진행상황을 업데이트합니다.
  • itertion값이 15이기 때문에 15번 학습을 진행하고 최적화된 item_factoruser_factor를 구할 수 있습니다.

추천 상품을 받을 때 als_model.recommend(item, click_sparse)로 사용하여, MatrixFactorizationBase에 있는 recommend 함수를 불러오게 됩니다.

def recommend(
        self,
        userid,
        user_items,
        N=10,
        filter_already_liked_items=True,
        filter_items=None,
        recalculate_user=False,
    ):
        ids, scores = self._knn.topk(self.item_factors, self.user_factors[userid], count)
        return list(
            itertools.islice((rec for rec in zip(ids[0], scores[0]) if rec[0] not in liked), N)
        )

recommend 함수의 경우 userid와 가장 연관성이 높은 userid implicit.gpu.KnnQuery().topk 함수로 추천하게 됩니다. KnnQuerycython으로 작성된 코드이고, 간단하게 설명하자면 user_factor matrix에서 코사인 유사도가 높은 user를 채택하는 방식입니다.

 

하지만 저희가 처음 학습을 돌릴때 sparse_matrix에 전치를 하고 학습을 한다면 useriditemid가 되고, recommend에서는 itemid를 넣었을 때 가장 유사한 itemid를 추천함으로써 상품에 따라 연관된 상품을 추천할 수 있게됩니다.

data = df[["user_id", "content_id"]]
data['rate'] = 1

user2idx = {}
idx2user = {}
for idx, user in enumerate(df['user_id'].unique()):
    user2idx[user] = idx
    idx2user[idx] = user

content2idx = {}
idx2content = {}
for idx, content in enumerate(df['content_id'].unique()):
    content2idx[content] = idx
    idx2content[idx] = content

useridx = data['user_id'].apply(lambda x: user2idx[x]).values
contentidx = data['content_id'].apply(lambda x: content2idx[x]).values
rating = data['rate'].values

als_model = ALS(factors=20, regularization=0.08, iterations = 20)
als_model.fit(click_sparse.T)
als_model.recommend(2, click_sparse)[0:10]

solver부분에 대한 설명은 코드가 너무 복잡해서 간략하게 넘어갔는데 혹시 더 자세히 알고싶으신 내용이 있으시면, 댓글라 질문 달아주세요. 처음으로 소스코드 분해를 해봤는데, 생각보다 오래걸렸습니다. 좋은 경험인거 같다는 생각이 들고, 여러분들도 소스코드를 보면 하나씩 해석하면서 코드를 이해하면 좋을 것 같다는 생각을 합니다. 

 

 

 

반응형

댓글