본문 바로가기

머신러닝, 딥러닝

고양이 사료 추천 시스템 만들기 : 여러 아이템 기반 추천(Item-item collaborative filtering)

내가 키우는 고양이는 입맛이 까다롭다. 습식을 한 박스로 시켜도 반은 잘 안 먹거나, 입에도 대지 않는 사료들이 있었다.

그래서 그때부터 우리 고양이가 잘 먹는, 안 먹는 사료를 기록하기 시작했다.

 

이번 2주간의 프로젝트로 "우리집 고양이의 입맛을 기반으로 하는 사료 추천"을 컨셉으로 잡았다.

방법은 크게 2가지가 있다.

1) 비슷한 입맛의 다른 고양이가 좋아한 사료 추천

2) 우리 고양이가 좋아한 사료와 비슷한 사료 추천

 

여기서 2) 아이템 기반 추천은 보통 한 아이템을 기반으로 한 추천(고양이의 선호도와 상관없다)이 많아서,

고양이 선호도를 반영한 아이템"들"을 기반으로 새 아이템들을 어떻게 추천할지 고민하면서 한참을 찾았다.

명쾌히 나오는 건 없었다. 

다만, 고양이 1이 아이템 A, B, C, D, E에 대해 각각 5, 5, 3, 3, 1의 별점을 매겼다면(별점은 1-5까지 있다.)

- 취향을 합산하고

- 좋아하는 아이템(5점)의 특징, 그저 그랬던(3점) 아이템의 특징, 안 좋아한(1점) 아이템의 특징을 분류하여

- 전체 아이템 기반으로 유사도를 평가할 수 있을 것이라 생각했다.

그리고 실제로 됐다!

다만 흥미롭고 이상한 점은, 코랩에서 돌리면 결과물이 고정되는데 비해

로컬에서 파이썬 파일로 돌리면 결과물이 그때그때 다르게 나온다. (하지만 오히려 이 추천결과가 더 마음에 든다.)

>> 이 점은 문제 원인을 찾았다. 아래에 적겠음.

그리고 이미 평가된 아이템도 추천하는 경우가 있었다. (10개를 추천한다면 2개 정도. )

>> 이 점은 필터링을 통해서 해결했다.

완벽한 알고리즘은 아니니, 차차 수정하고 추가할 예정이다.

먼저, 상품 정보가 담긴 데이터프레임(df)와 상품의 별점 정보가 담긴 데이터프레임(rating)이 필요하다.

df
rating

df['soup'] = df['brand'] +' ' + df['title']  +' ' + df['classification']  +' ' + df['content']  +' ' + df['info']

먼저 df에서 상품의 유사도를 구할 때 쓰이길 원하는 칼럼을 합해준다. 이 칼럼을 dtm으로 만들 예정이다.

cat_rating = rating[['고양이_ID','rating', 'title']]
combined_foods_data = pd.merge(df,cat_rating, on='title', how='left')

그리고 rating과 df를 left merge 해준다. (상품 정보와 평가 정보를 합친다.)

rating_crosstab = combined_foods_data.pivot_table(values='rating', 
                                                  index='고양이_ID', columns='soup', fill_value=0) # 없는 값은 0으로 채우기.

 

이렇게 상품의 정보를 합한 칼럼과, 그 칼럼에 대한 고양이들의 평가를 피봇 테이블로 만든다.

 

다음은 선호도 정보를 반영한다.

고양이가 좋아했던, 그저 그랬던, 별로였던 사료를 구분한다.

rating_crosstab['favorite'] = ""
rating_crosstab['soso'] = ""
rating_crosstab['no'] = ""

for i in range(len(rating_crosstab)):
  for j in range(len(rating_crosstab.columns)):
    try:
      if int(rating_crosstab.iloc[i,j]) >= 4 : # 2,5,5,1,0,5,0,...
        rating_crosstab.iloc[i,-3] += rating_crosstab.columns[j] + ' '
      elif 3<= int(rating_crosstab.iloc[i,j]) <4 :
        rating_crosstab.iloc[i,-2] += rating_crosstab.columns[j] + ' '
      elif 1<= int(rating_crosstab.iloc[i,j]) < 3:
        rating_crosstab.iloc[i,-1] += rating_crosstab.columns[j] + ' '
      else:
        pass
    except: pass

위 코드를 Tranpose한 버전.

 

일단 2번 고양이의 입맛을 분석해보겠다.

2.0 에는 "고양이_ID" 가 들어간다.

fav = rating_crosstab.loc[2.0,'favorite']
ss = rating_crosstab.loc[2.0,'soso']
nn = rating_crosstab.loc[2.0,'no']

TF를 계산해줄 것이다.

from math import log
docs = [fav, ss, nn]
vocab = list(set(w for doc in df2['soup'].tolist() for w in doc.split())) # 전체 df의 단어를 vocab으로 만들어야 한다.
vocab.sort()

먼저 고양이의 입맛 정보에 대한 DTM을 만들지만, 단어는 전체 df의 단어로 DTM을 만들어야 한다.

그래야 뒤에서 코사인 유사도로 계산이 되기 때문이다.

여기서 sort()를 생략하면, 추천 모델을 돌릴 때마다 추천 결과가 바뀐다. 정렬을 안 해주었기 때문일까.

 

다음은 TF, IDF의 계산 식이다. 

N = len(docs)

def tf(t, d):
  return d.count(t)

def idf(t):
  df = 0
  for f in fav:
    df += t in f
  return log(N)/(df+1)

def tfidf(t, d):
  return tf(t,d) * idf(t)

그리고 TF를 구해 데이터프레임 DTM 형식으로 출력한다.

# tf 구하기 - DTM 데이터프레임에 저장하여 출력

result = []
for i in range(N):
  result.append([])
  d = docs[i]
  for j in range(len(vocab)):
    t = vocab[j]
    result[-1].append(tf(t,d))

tf_ = pd.DataFrame(result, columns = vocab, index=['fav', 'soso', 'no'])

 

아까 합친 'soup' 칼럼에 대한 DTM 별로 고양이의 선호도 정보가 나온다.

계산하기 쉽게 전치를 한번 해주고, 고양이 선호도에 따른 가중치를 부여한다.

tfsum = tf_.T
tfsum['sum'] = ((tfsum['fav'] * 2) + (tfsum['soso'] * 1) + (tfsum['no'] * 0)) / 3
# 안 먹는걸 0으로 하는 이유는 코사인 유사도 할때 내적을 하기 때문에 절대값이 계산되기 때문. 그 영향을 없애고자.

잘 안 먹는 사료(1점)은 0점을 부여하여 곱해, 영향을 없앤다.

 

마이너스를 해주는 게 아니라 0인 이유는 코사인 유사도 계산 시, 내적의 절대값으로 반영되어서 점수가 올라가게 하지 않기 위함이다. 원래 마이너스를 해주다가 이렇게 바꾸니 결과가 더 좋았다.

다 0으로 보이는건, 전체 데이터프레임의 단어 양이 많아서 그렇다. 

다시 한번 전치해주고, 합계만 남기면 고양이의 선호도 정보만 남는다.

avgdtm = tfsum[['sum']]
avgdtm = avgdtm.T

 

이제 기존 df에 있는 사료들과의 코사인 유사도를 계산한다.

df2 = df[['index', 'title', 'soup']]

df에서 필요한 정보 - 인덱스, 사료명, 합친 칼럼만 남겨 새 df에 복사한다.

위와 똑같은 계산을 해서, dtm을 만든다.

result = []
for i in range(N):
  result.append([])
  d = docs[i]
  for j in range(len(vocab)):
    t = vocab[j]
    result[-1].append(tf(t,d))

tf_2 = pd.DataFrame(result, columns = vocab, index=df2.title.tolist())
tf_2

사료별로 들어간 것을 볼 수 있다.

 

이제 개별 행(사료)에 대해 아까 구했던 고양이 입맛 dtm과 코사인 유사도를 구한다.

from numpy import dot
from numpy.linalg import norm
import numpy as np
def cos_sim(A, B):
  return dot(A, B) / (norm(A)*norm(B))

sim_scores = []
for i in range(len(tf_2)):
  sim_scores.append((i,cos_sim(avgdtm, tf_2.iloc[i])))
sim_scores

sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)
sim_scores = sim_scores[1:11]

유사도 기반으로 정렬하고 상위 10개만 남긴다.

food_indices = [i[0] for i in sim_scores]
df2['title'].iloc[food_indices]

개별 항목을 출력해주면 다음과 같은 결과가 나온다.

추천 완료!