본문 바로가기
추천시스템

메모리 기반 하이브리드 필터링 구현

by 볼록티 2020. 2. 6.
728x90
반응형

 An Approach for Combining Content-based and Collaborative Filters(2002) 논문에서 나와 있는 방법을 토대로 구현을 해본다. 다소 부족하고 틀린 부분이 있을 수 있는 점을 사전에 알리는 바입니다.

 방법론에 대한 논문을 살펴본 내용은 다음 페이지에 있다. 다음페이지에 것은 2006년도 것이고, 여기서 다룰 알고리즘은 그 전인 2002년의 것을 가지고 진행한다. 2002년에 것은 하이브리드 필터링인데 마지막에는 협력 필터링의 알고리즘을 가지고 추천리스트를 생성하는데, 아이템기반으로 하는 것이다. 물론 사용자 기반으로도 할 수 있다.

 혹시라도 틀린부분이 있다면 꼭 지적을 해주셨으면 하는 바램이 있습니다..

https://data-science-hi.tistory.com/76

 

A new approach for combining content-based and collaborative filters

하고자 하는 것 콘텐츠 기반 필터링과 협업 필터링의 각 장점을 활용하여 나은 성능을 달성하는 새로운 필터링 접근법을 소개하고자 한다. 사용자 프로필과 사용자 평점(rating)에서 추출한 통합 정보를 기반으로..

data-science-hi.tistory.com

 데이터 셋은 무비렌즈 데이터 중 가장 작은 데이터셋인 분석용 데이터 10만개의 평가, 943명의 사용자 1682개의 영화로 이루어진 것을 사용하였다.

 

 1. 데이터 불러오기

https://grouplens.org/datasets/movielens/ 사이트에 있는 무비렌즈 데이터를 불러와 누구나 사용할 수 있다.

 

MovieLens

GroupLens Research has collected and made available rating data sets from the MovieLens web site ( The data sets were collected over various periods of time, depending on the size of the set. …

grouplens.org

Index of unzipped files 에 들어가면 아래처럼 되어있는데 여기서 u.data랑 u.item을 가져왔다. 다운을 받고 텍스트로 다시 저장해서 쉽게 파이썬내로 불러올 수 있다.

 

 

 

데이터 설명

 

필요한 모듈.

import pandas as pd
import math
from sklearn.cluster import KMeans
from sklearn.metrics import pairwise
import numpy as np

 받은 데이터파일은 DATA파일 형식이라서 텍스트파일로 바꾸고 불러왔다.

rating = pd.read_csv("u.data.txt", sep='\t', names=['user_id','item_id','rating','timestamp'])
movie = pd.read_csv("u.item.txt",sep='|', names=['item_id','movie_title','release_date','video_release_date',
                                                 'IMDb_URL','unknown','Action','Adventure','Animation','''Children's''', 
                                                 'Comedy','Crime','Documentary','Drama','Fantasy','Film-Noir','Horror', 
                                                 'Musical','Mystery','Romance','Sci-Fi','Thriller','War','Western'])
                                                 

 

 

불러온 데이터

rating.head()
movie.head()

 

 

 

 

<- rating 데이터

movie 데이터

 

불필요한 컬럼 제거

rating1 = rating[['user_id','item_id','rating']]
rating1

movie1 = movie[['item_id','Action','Adventure','Animation','''Children's''', 
                                                 'Comedy','Crime','Documentary','Drama','Fantasy','Film-Noir','Horror', 
                                                 'Musical','Mystery','Romance','Sci-Fi','Thriller','War','Western']]
movie1                                                 

 

 

2. Item-rating 행렬과 Group-rating 행렬 만들기

2.1. Group rating 만들기

Group rating 행렬을 만들기 위해서는 콘텐츠의 정보가 담긴 movie데이터 셋을 활용한다. 콘텐츠 정보를 토대로 영화를 그루핑하는데 사용하는 알고리즘은 k-means 이고, 이 포스팅에서는 k값을 5으로 하였다.

 

아래의 식을 사용하여 각 객체가 k 클러스터에 속할 확률을 구하게 된다. $Pro(j,k)$는 객체 $j$가 클러스터 $k$에 속할 확률을 나타내고, $CS(j,k)$ 는 $counter-similarity$라고 해서 유사도를 구한다. 

 

$Pro(j,k) = 1 - \frac{CS(j,k)}{MaxCS(i,k)}$

 

 

 

K-means 로 군집화 하기

movie2에 군집컬럼을 추가해준다.

model = KMeans(n_clusters=5)
model.fit(movie1.iloc[:,1:])
y=model.fit_predict(movie1.iloc[:,1:])
movie2=movie1
movie2['cluster'] = y

 

 

클러스터 별로 평균 벡터 구하기

각 클러스터 별로 평균값 벡터를 만들어서 모든 객체와 유사도를 구할 수 있게 준비한다.

# 클러스터 별로 평균 벡터 구하기
c_number = list(set(movie2['cluster']))
c_mean = []
for i in c_number:
    tmp = movie2[movie2['cluster'] == i]
    m = list(tmp.iloc[:,1:].mean())
    c_mean.append(m)

 

각 객체별로 모든 클러스터와의 counter-similarity 구하기.

본 포스팅에서 counter-similarity는 코사인 유사도를 사용하였다. 논문에서는 코사인이나 유클리디안을 사용한다고 명시되어있다. 코사인 유사도는 이전 포스팅에서 만든 코드를 그대로 사용하였다.

https://data-science-hi.tistory.com/73?category=1077184

 

MovieLens 데이터를 활용한 Collaborative Filtering 구현

MovieLens 데이터를 가지고 CF 의 기법인 사용자 기반, 아이템 기반 필터링을 구현해보도록 한다. 추천시스템 연구 분야의 경우 성능을 평가하거나 다른 알고리즘의 변형을 공식적인 데이터들을 통해서 비교하고,..

data-science-hi.tistory.com

def cosine_similarity(A,B): # ex) A=[2.0, 3.0] , B=[5.0, 3.5] ; 리스트 속 수치형 자료, 당연히 차원이 같아야함.
    dot_p = np.dot(A,B)
    A_norms = math.sqrt(sum([i**2 for i in A]))
    B_norms = math.sqrt(sum([i**2 for i in B]))
    AB_norms = A_norms * B_norms
    
    return dot_p / AB_norms
# 각 객체별로 모든 클러스터와의 counter-similarity  구하기
k_sim =[]
for i in range(len(movie2)):
    tmp=[]
    for j in c_mean:
        tmp.append(cosine_similarity(list(movie2.iloc[i,1:-1]),j))
    k_sim.append(tmp)
pd.DataFrame(k_sim)

 

 

아래의 결과처럼 각 객체별로 클러스터와의 유사도를 구하게 된다. 이제 제안한 공식을 활용해서 각 객체별로 $Pro(j,k)$를 구해보도록 하자.

 

 

각 객체별로 $Pro(j,k)$ 구하기.

$Pro(j,k) = 1 - \frac{CS(j,k)}{MaxCS(i,k)}$

 

논문에서는 counter-similarity를 유클리디안 거리나 코사인 방법을 사용한다고 했는데, 아마 거리를 구하기 때문에 " 1 - " 즉, 1에서 거리를 뺀 값을 유사도로 측정한 것 같다. 애초에 볼록티는 유사도를(1에 가까울수록 유사함)구했기 때문에 " 1 - " 부분을 생략하였다.

# 각 객체 별로 클러스터 속할 확률 구하기

#1. 각 클러스터 별 유사도 최댓값 구하기
sim_d = pd.DataFrame(k_sim)
max_k = []
for i in range(5):
    max_k.append(max(list(sim_d.iloc[:,i])))
    
    
#2. 공식을 이용하여 Pro 값 산출하기
pro = []
for i in range(len(sim_d)):
    tmp = []
    for j in range(len(max_k)):
        tmp.append(sim_d.iloc[i,j]/max_k[j])
    pro.append(tmp)

pro_d = pd.DataFrame(pro, columns=['c1','c2','c3','c4','c5'])
pro_d

아래의 데이터프레임처럼 각 객체별로 클러스터에 속할 확률을 제안한 fuzzy k-means 로 모두 구해보았다. 여기서 알아낸 점은 각 값들이 위의 기존의 코사인 유사도와 비교했을 때 조금씩 수치가 상승된 사실을 알 수 있었다. 이로써 수치를 올려 가중을 주게 된 것인가 라는 생각을 해본다. 

Group-rating 결과

 

 

Group-rating 행렬도 만들고 아래처럼 Item-rating 행렬도 만든다. 이 두 행렬에서 각각 유사도를 구하고, 제안한 공식대로 최종 유사도를 구할 수 있다.

pd.pivot_table(rating1, values='rating', index=['item_id'], columns=['user_id']).fillna(0)

Item-rating 표

 

 

2.2. 유사도 구하기

유사도를 구하는 단계이다. 여기서는 세가지 단계로 크게 구분이 되어 있다. 1) 피어슨 상관계수 2) 조정된 코사인 유사도 3) 유사도 선형결합. 으로 구성된다.

 

1) 피어슨 상관계수

논문에서는 Item-rating 행렬을 통해 아이템 간의 유사도를 구하는 경우에 피어슨 상관계수를 사용하였다. 

피어슨 상관계수를 활용한 유사도

상관계수를 구할 때 $\bar{R}_{k}$와 $\bar{R}_{l}$ 은 사용자 $u$ 와 상관없이 가지고 있는 모든 평점에 대한 평균으로 구하였다. 즉, 0이어도 평균 계산에 포함시켰다. 그렇지 않으면 문제가 발생한다. 바로 Zero Devision 문제이다. 이 문제는 분모가 0일 경우 계산에서 오류가 난다는 것을 알리는 메세지인데, 두가지의 경우가 발생하기 때문이다. 첫째, 평점이 하나만 있을 경우에 분모가 0이 된다. 둘때, 평점이 두 개 이상이여도 모두 같으면 분모가 0이 될 수 밖에 없다. $k$ , $l$ 둘 중하나만 그래도 오류가 난다. 그래서 나는 평균은 전체 $k$, $l$ 가 가진 (0도포함) 평점들의 평균을 $\bar{R}$로 사용하였다.

 

 

아래는 상관계수를 구하는 함수이다. 평균을 따로 x_m과 y_m으로 구해서 넣게 했는데, 이는 위에서 설명한 것 처럼 오류를 방지하기 위해서 동시에 평가된 것들에 대한 리스트인 x, y와 계산되는 값이 다르기 때문이다.

def corelation(x, y, x_m, y_m): #[1,2,3], [4,5,6], 2, 5
    if len(x) != len(y):
        print('길이가 다름')
        return
    if len(x)==0:  # 동시평점이 없기에 0으로 대체
        print('공통된 게 없음')
        return 0
    
    # 평균
    x_avg=x_m
    y_avg=y_m
    
    # 두 변수 각각 오차 제곱의 합
    x_resi_2=sum([(float(i)-x_avg)**2 for i in x])
    y_resi_2=sum([(i-y_avg)**2 for i in y])
    
    # 두 변수의 각 오차 곱의 합
    x_y_resi = sum([(i-x_avg)*(j-y_avg) for i,j in zip(x,y)])
    return (x_y_resi) / (math.sqrt(x_resi_2)*math.sqrt(y_resi_2))

 

아래의 코드는 result 안에 아이템간의 유사도를 차례로 구해서 리스트 안에 리스트로 넣게 된다. 그리고 데이터프레임으로 만들면 바로 아래의 결과표처럼 나타나게 된다.

def corelationship(data):    
    a=time.time()
    result=[]
    for i in data.index:
        actor_cor=[]
        actor = set([a for a,b in zip(data.columns, data.loc[i,:]) if b != 0]) # actor user 평점 컬럼
        for j in data.index:
            #공통컬럼찾기
            nei = set([a for a,b in zip(data.columns, data.loc[j,:]) if b != 0]) # neighbor user 평점 컬럼
            same=list(actor.intersection(nei))

            #i와j 의 평균
            i_m = sum(data.loc[i,:])/len(data.loc[i,:])
            j_m = sum(data.loc[j,:])/len(data.loc[j,:])

            #상관계수를 구하자.
            x=list(data.loc[i,same])
            y=list(data.loc[j,same])
            actor_cor.append(corelation(x,y,i_m,j_m))


        # actor 와의상관계수를 담자.
        result.append(actor_cor)    
    print(time.time()-a)
    return result

아래의 보이는 것과 같이 200개에 대한 예시이다.

 

 

 

 

2) 조정된 코사인 유사도

 논문에서는 Group-rating 행렬을 통해 아이템 간의 유사도를 구하는 경우에 조정된 코사인 유사도를 사용하였다. 아래의 공식 생긴 것만봐도 위의 피어슨 상관계수랑 거의 흡사해 보인다. 코사인 유사도는 다른 등급에서의 벡터들이 계산되는걸 제외하면 피어슨 상관계수와 매우 유사하다. 아래의 식은 결국 위의 구했던 피어슨 상관계수와 같기 때문에 같은 코드를 사용하여 유사도를 구하였다. 

 논문에도 추천시스템에서의 수식에 대한 설명을 하였고, 이를 적용하여 위의 것과 그대로 적용하였다.

 

 

코드가 간결하지 못하여 위에 상관계수를 구하나 아래의 것을 구하나 각각 1분정도 걸린다. (200케이스에 대해서...) 만약 전체를 다한다면 정말 오래 걸릴 것이다. 전체에 대해 다 하는 것은 최종적으로 결과로 나타내도록 하였다.

 

 

 

3) 유사도 선형결합

 유사도를 구한 두 행렬을 가지고, 하나의 유사도 행렬로 만들어 주는 작업을 시행한다. 이 논문이 제안한 선형결합 방법은 아래의 식에 따라 계산하면된다. $c$ 값은 파라미터 값이고, 초기에 0.5로 각각 가중치를 같게 준다. 이후에 $c$를 0.1씩 감소시키면서 추천시스템의 성능을 봐가면서 조절한다. 왜 감소먼저하냐하면 초기에는 item rating 행렬이 sparse할 수 있다. 하지만 이는 점점 시간이 흘러 사용자들의 평점으로 채워질 것이고, 갈수록 가중치를 더 주겠다는 의도로 해석했다.

 

제안한 선형결합

 아래의 코드는 item rating 행렬과, group rating 행렬 그리고 초기 c=0.5로 설정한 선형결합을 통해 최종 유사도 행렬을 만드는 함수다. 함수를 실행한 결과는 아래와 같다. 계산시간은 작은 데이터로 했기 때문에 별로 안걸린다.

def linear_combination(item, group, c=0.5):

    a=time.time()
    l_com = []
    for i in item.index:
        tmp=[]
        for j in item.columns:
            i_r = item.iloc[i,j]
            g_r = group.iloc[i,j]
            c_sim = i_r*(1-c) + g_r*c
            tmp.append(c_sim)
        l_com.append(tmp)
    return l_com
    print(time.time()-a)

 

 

$item \times item$ 행렬 임.

하이브리드 필터링 유사도 행렬
사용자 평점 유사도 행렬

 

하이브리드 필터링 유사도 행렬은 확실이 기존의 사용자 평점 유사도 행렬보다 범위가 다양해진 듯 하다. 사용자 평점 유사도 행렬은 동시 평점이 매겨진 아이템에 대해서 다루기 때문에 유사도가 전체적으로 높게 나오는 경향도 없지않다 있더라.

 

 

4) 콜드 스타트 문제

 새로운 아이템에 대해서는 평점이 없고 그렇게 되면 $\bar{R}$ 이 0이 되는데, 이러면 예측 평점을 만들 때 문제가 되기 때문에, 이를 group-rating 행렬로 부터 해결하게 되는 것이 목표이며, 이를 가지고 실험한 결과 역시 더 나은 성능을 보였다고 함. 

 

2.3. 예측 평점 구하기

$\bar{R}_{k}$ 는 평점이 있는 것들의 평균으로 구했다.

 

# 사용자를 입력하면 사용자가 평점을 매기지 않은 것들에 대해 예측 평점을 내림차순으로 출력하여 추천하기.

def hybrid_recommendation(data, user, n=5):

#1. 매개변수로 받은 user가 평가한 영화의 인덱스와 평가하지 않은 인덱스를 각각 yes, no로 가져온다.
    yes = [(i,j)[0] for i,j in enumerate(data.iloc[:,user-1]) if j != 0]  # 평점을 매긴 영화의 인덱스
    no = [(i,j)[0] for i,j in enumerate(data.iloc[:,user-1]) if j == 0] # 평점을 매기지 않은 영화의 인덱스
    
#2. 추천할 아이템들인 i 와의 유사도를 내림차순하여 n개만 뽑음. i와의 유사도를 구할 k들은 actor사용자가 평가한 다른 아이템임 과의 유사도임.
    nei_sim = []
    t=[]
    answer= [] # 최종 예측

    for i in no:
        

        
        nei_sim= pd.DataFrame(sim_df.iloc[i,yes]).sort_values(by=i, ascending=False) 
        a= list(nei_sim.index[:n]) # 상위 n 이웃 수집하기

        #  동시평점 아이템의 유사도 평균
        sum_mean = sum(sim_df.iloc[i,a])/len(sim_df.iloc[i,a])

        nei_resid=[]
        for p in a:
            nei_resid.append((data.iloc[i,p] - sum_mean) * sim_df.iloc[i,p])

        R=[x for x in data.iloc[i,:] if x != 0]
        answer.append(((sum(R) / len(R)) + sum(nei_resid),i))
    
    return pd.DataFrame(answer).sort_values(by=0, ascending=False)
hybrid_recommendation(data,194, n=5)

 

아래와 같은 결과에 첫번째 컬럼은 예측치이고, 두번째 컬럼은 아이템 넘버인데, 잘못된 부분은 바로 예측 평점이여야 하는데 그 범위를 벗어난 값들이 있다. 

 그 이유는 바로 $\bar{R}$의 분모의 범위를 설정을 유사도를 계산할 때와 예측치를 구하는 부분이랑 다르게 해서 그렇다. 예를 들어 'zero devision' 같은 오류들을 모두 고려하지 않았기 때문이라고 생각한다. 

 

2.4. 콜드 스타트 문제 대처 방안

 콜드스타트. 즉 협업필터링에서 사용자에 대한 정보가 초기에 부족하여 추천이 불가능하거나 정확도가 매우 떨어지는 경우를 말하며, 콘텐츠의 정보를 활용하여 이러한 sparse를 메울 수 있게 한다. 또한 이 논문에서 새로운 아이템에 대해 평점이 아예 존재하지 않으므로 새로운 아이템에대해서 $\bar{R}_k$ 대신에 $\bar{R}_{neighbor}$를 사용한다.

 

 

느낀점. 구현을 하면서 느낀건 이미 어느정도 알고리즘에 대한 이해가 충분히 되어 있어야겠다. 저자 또한 그것을 감안하고 작성한 것일테니. 사실상 협업필터링에 대한 단순한 예제는 구현이 가능하지만, 이렇게 조금 응용된 부분들을 완벽하게 구현하기란.. 여간 .. 쉬운게 아니다. 

 즉! 기초를 탄탄히! 완벽한 이해만이 응용에서 구현을 할 수가 있는 것이다.

 

상위 5개의 클러스터들.

 

수식을 코드로 옮김에 아쉬움이 많이 남은 연습이었지만, 아무쪼록 생각할 점, 부족할 점, 배운 점이 크다. 구현을 하고 안하고의 차이가 크다... 완벽하진 않아도 계속 도전하쟝

728x90
반응형

댓글