유사상품 추천

유사상품 추천은 인터넷 사이트에서 흔히 사용되는 아주 효과적인 방법이다. 음악스트리밍 사이트에서 베토벤 교향곡을 듣고 나면 다른 클래식 음악을 이어 들을 수 있고, 유튜브(Youtube)에서 어떤 동영상을 감상하고 나서 유사한 다른 동영상을 이어서 감상할 수 있다. 쿠팡이나 아마존(Amazon)과 같은 온라인 상거래 사이트에서 어떤 상품의 구매를 고려할 때 유사한 다른 상품들과 비교해 보는 것이 구매 결정에 도움이 된다.

유사상품을 어떻게 찾을까? 두 상품을 어떤 경우에 유사하다고 하고 어떤 경우에 유사하지 않다고 할까? 추천 시스템에서 구현하기 위해서 유사성의 정도를 수치화한다. 두 상품 A와 B가 있을 때, 유사함의 정도를 나타내는 어떤 함수 f(A,B)f(A,B)를 구체적으로 정함으로써 그 값이 높을 수록 상품간 유사성이 높다고 하는 것이다. 유사성 함수를 정의하고 나면 특정 상품에 대한 추천 유사상품을 찾을 때, 그 상품과 다른 상품들 간의 유사성 함수 ff를 평가해 본 후, 결과가 가장 높은 상품들을 고른다.

유사성을 판단하기 위해 사용하는 정보를 크게 두 종류로 나눌 수 있다. 첫번째는 상품의 속성정보(metadata) 두번째는 상품의 이용정보이다.

속성정보의 이용

속성정보란 판매되는 상품의 속성을 말한다. 좋은서점닷컴에서 판매하는 서적의 예를 들면, 책의 저자, 출판년도, 출판사, 페이지수, 내용상의 분류기호 등이 속성정보에 해당한다. 두 서적 사이의 유사성을 책의 저자가 같은지, 출판년도가 비슷한지, 페이지 수가 비슷한지, 분류기호가 같은지 등으로 평가한다면 속성정보를 이용한 유사성 판단이 된다. 예를 들어 다음과 같은 세 서적이 있다고 생각해 보자.

서적1서적2서적3
저자홍길동홍길동임꺽정
출판년도200020102000
출판사가나다마바사마바사
페이지수1001100100
분류기호문학역사문학

간단한 예로 한 가지 속성정보만을 바탕으로 하는 유사성 함수들을 가중평균하는 방법을 들 수 있다. 예를 들어 한 가지 속성정보만을 사용하는 함수들을 다음과 같이 생각하고,

f저자_유사성(A,B)=A와 B가 동일한 저자의 책이면 1, 아니면 0f출판년도_유사성(A,B)=log10A의 출판년도와 B의 출판년도 차이의 절대값f출판사_유사성(A,B)=A와 B가 동일한 출판사의 책이면 1, 아니면 0f페이지수_유사성(A,B)=log10A의 페이지수와 B의 페이지수 차이의 절대값f분류기호_유사성(A,B)=A와 B의 분류기호가 같으면 1, 아니면 0\begin{align*} f_{저자\_유사성}(A, B) &= A와\ B가\ 동일한\ 저자의\ 책이면\ 1,\ 아니면\ 0 \\ f_{출판년도\_유사성}(A, B) &= - \log_{10}{A의\ 출판년도와\ B의\ 출판년도\ 차이의\ 절대값} \\ f_{출판사\_유사성}(A, B) &= A와\ B가\ 동일한\ 출판사의\ 책이면\ 1,\ 아니면\ 0 \\ f_{페이지수\_유사성}(A, B) &= - \log_{10}{A의\ 페이지수와\ B의\ 페이지수\ 차이의\ 절대값} \\ f_{분류기호\_유사성}(A, B) &= A와\ B의\ 분류기호가\ 같으면\ 1,\ 아니면\ 0 \\ \end{align*}

유사성 함수를 이들의 가중평균으로 정의해 보자

f가중평균_유사성(A,B)=C1×f저자_유사성(A,B) +C2×f출판년도_유사성(A,B) +C3×f출판사_유사성(A,B) +C4×f페이지수_유사성(A,B) +C5×f분류기호_유사성(A,B)\begin{align*} f_{가중평균\_유사성}(A, B) &= C_1 \times f_{저자\_유사성}(A, B) \\ &\ + C_2 \times f_{출판년도\_유사성}(A, B) \\ &\ + C_3 \times f_{출판사\_유사성}(A ,B) \\ &\ + C_4 \times f_{페이지수\_유사성}(A, B) \\ &\ + C_5 \times f_{분류기호\_유사성}(A, B) \\ \end{align*}

여기서 C1C_1, C2C_2, …, C5C_5 는 양의 (positive) 값을 갖는 매개변수들로 각 속성별 유사성의 중요도를 반영하여 결정한다. 가령, C1C_1=10.0, C2C_2=1.0, C3C_3=1.0, C4C_4=1.0, C5C_5 =2.0 라고 정하고 서적1을 기준으로 유사성 함수 값을 계산해 보면 아래와 같다.

f가중평균_유사성(서적1,서적2)=C1×f저자_유사성(홍길동,홍길동) +C2×f출판년도_유사성(2000,2010) +C3×f출판사_유사성(가나다,마바사) +C4×f페이지수_유사성(100,1100) +C5×f분류기호_유사성(문학,역사)=10×1+1×(1)+1×0+1×(3)+2×0=1013=6\begin{align*} f_{가중평균\_유사성}(서적1, 서적2) &= C_1 \times f_{저자\_유사성}(홍길동, 홍길동) \\ &\ + C_2 \times f_{출판년도\_유사성}(2000, 2010) \\ &\ + C_3 \times f_{출판사\_유사성}(가나다, 마바사) \\ &\ + C_4 \times f_{페이지수\_유사성}(100, 1100) \\ &\ + C_5 \times f_{분류기호\_유사성}(문학, 역사) \\ &= 10 \times 1 + 1 \times (- 1) + 1 \times 0 + 1 \times ( - 3) + 2 \times 0 \\ &= 10 - 1 - 3 \\ &= 6 \\ \end{align*}
f가중평균_유사성(서적1,서적3)=C1×f저자_유사성(홍길동,임꺽정) +C2×f출판년도_유사성(2000,2000) +C3×f출판사_유사성(가나다,마바사) +C4×f페이지수_유사성(100,100) +C5×f분류기호_유사성(문학,문학)=10×0+1×0+1×0+1×0+2×1=10+2=12\begin{align*} f_{가중평균\_유사성}(서적1, 서적3) &= C_1 \times f_{저자\_유사성}(홍길동, 임꺽정) \\ &\ + C_2 \times f_{출판년도\_유사성}(2000, 2000) \\ &\ + C_3 \times f_{출판사\_유사성}(가나다, 마바사) \\ &\ + C_4 \times f_{페이지수\_유사성}(100, 100) \\ &\ + C_5 \times f_{분류기호\_유사성}(문학, 문학) \\ &= 10 \times 0 + 1 \times 0 + 1 \times 0 + 1 \times 0 + 2 \times 1 \\ &= 10 + 2 \\ &= 12 \\ \end{align*}

계산 결과에 따르면 서적1과 서적3의 유사함수 값이 서적1과 서적2의 유사함수 값보다 높다는 것을 알 수 있다. 가중평균법을 사용하기 위해서는 각 속성별 유사성 함수를 정의하고 가중평균 매개변수들(C1C_1, C2C_2, …, C5C_5 )을 적절히 선택해야 한다.

피처벡터

가중평균보다 좀더 일반적인 방법은 각 상품을 피처벡터(feature vector)로 표현하는 방법이다. 피처벡터(feature vector)는 각 상품을 실수(real number)값의 벡터(vector)로 나타낸 것으로 다양한 기계학습(machine learning) 방법들의 기반이 된다.

서적1, 서적2, 서적3의 예를 들어 설명해 보자. 다섯가지 속성 가운데 저자, 출판년도와 페이지수는 숫자 정보 (numeric) 이고 저자, 출판사, 분류기호는 숫자가 아닌 (non-numeric) 정보이다. 먼저 숫자 정보만 사용하는 경우를 살펴보자. 출판년도와 페이지수 두 숫자 정보를 그대로 피처벡터로 사용하면 다음과 같다.

피처의 정의
피처1출판년도
피처2페이지수
피처벡터
서적1(2000100)\begin{pmatrix}2000 & 100\end{pmatrix}
서적2(20101100)\begin{pmatrix}2010 & 1100\end{pmatrix}
서적3(2000100)\begin{pmatrix}2000 & 100\end{pmatrix}

각 서적을 피처벡터로 표현했고 피처벡터의 차원(dimension)은 2이다. 두 벡터간의 거리(distance)를 이용해 유사성을 계산하는데, 흔히 사용되는 방법으로 내적(dot product)과 코사인(cosine) 계산법이 있다. 내적 값이 크거나 코사인 값이 클 수록 유사성이 높다고 판단한다.

내적을 사용하여 서적1을 기준으로 유사성을 계산해 보면,

f내적_유사성(서적1,서적2)=2000×2010+100×1100=4,130,000f내적_유사성(서적1,서적3)=2000×2000+100×100=4,010,100\begin{align*} f_{내적\_유사성}(서적1, 서적2) &= 2000 \times 2010 + 100 \times 1100 = 4,130,000 \\ f_{내적\_유사성}(서적1, 서적3) &= 2000 \times 2000 + 100 \times 100 = 4,010,100 \end{align*}

이 된다. 하지만 이 방법에는 문제가 있다. 출판년도와 페이지수를 그대로 피처벡터로 사용하여서는 내적값이 유사성을 잘 표현하지 못하기 때문이다. 두 서적의 출판년도와 페이지수가 비슷할 수록 유사함수 값이 높아야 하는데, 좀전에 정의한 내적은 단순히 출판년도 값이 크거나 페이지 수가 크면 높아지기 때문이다. 이렇게 해서는 유사성을 적절히 표현할 수가 없다.

좀더 개선된 방법으로 다음과 같은 피처벡터를 생각해 보자. 이번에는 분류기호도 피처에 사용했다.

피처의 정의
피처1출판년도가 2000년 이전이면 1 아니면 0
피처2출판년도가 2000부터 2009년까지이면 1 아니면 0
피처3출판년도가 2010부터 2019년까지이면 1 아니면 0
피처4출판년도가 2020년 이후이면 1 아니면 0
피처5페이지수가 100미만이면 1 아니면 0
피처6페이지수가 100이상 1000 미만이면 1아니면 0
피처7페이지수가 1000이상이면 1아니면 0
피처8분류기호가 문학이면 1 아니면 0
피처9분류기호가 역사이면 1 아니면 0
피처10분류기호가 자연과학이면 1 아니면 0

이제 새로운 피처들을 사용하여 서적1, 서적2, 서적3의 피처벡터를 구해보자.

피처벡터
서적1(0100010100)\begin{pmatrix}0 & 1 & 0 & 0 & 0 & 1 & 0 & 1 & 0 & 0\end{pmatrix}
서적2(0010001010)\begin{pmatrix}0 & 0 & 1 & 0 & 0 & 0 & 1 & 0 & 1 & 0\end{pmatrix}
서적3(0100010100)\begin{pmatrix}0 & 1 & 0 & 0 & 0 & 1 & 0 & 1 & 0 & 0\end{pmatrix}

여기서 각 서적을 표현한 벡터의 차원(dimension)은 10이다. 새로운 피처벡터를 사용하여 내적을 다시 계산해 보면

f내적_유사성(서적1,서적2)=0×0+1×0+0×1+0×0+0×0+1×0+0×1+1×0+0×1+0×0=0f내적_유사성(서적1,서적3)=0×0+1×1+0×0+0×0+0×0+1×1+0×0+1×1+0×1+0×0=3\begin{align*} f_{내적\_유사성}(서적1, 서적2) &= 0 \times 0 + 1 \times 0 + 0 \times 1 + 0 \times 0 + 0 \times 0 + 1 \times 0 + 0 \times 1 + 1 \times 0 + 0 \times 1 + 0 \times 0 \\ &= 0 \\ f_{내적\_유사성}(서적1, 서적3) &= 0 \times 0 + 1 \times 1 + 0 \times 0 + 0 \times 0 + 0 \times 0 + 1 \times 1 + 0 \times 0 + 1 \times 1 + 0 \times 1 + 0 \times 0 \\ &= 3 \\ \end{align*}

이다. 앞서의 결과와는 달리 서적1과 서적3의 유사함수 값이 서적1과 서적2의 유사함수 값보다 높다는 것을 알 수 있다.

두번째 방법에서는 속성정보를 그대로 사용하는 대신 속성정보를 바탕으로 따로 정의된 피처들을 사용하였다. 이렇게 하면 단순히 출판년도 값이 크거나 페이지 수가 클수록 내적값이 높게 계산되는 문제가 없다. 또한 숫자로 된(numeric) 정보 뿐만 아니라 숫자가 아닌(non-numeric) 속성정보도 피처로 만들 수 있다. 쓸모 있는 피처를 고안하는 과정을 피처 엔지니어링(feature engineering)이라고 한다. 피처 엔지니어링은 추천 시스템에서 그리고 일반적으로 기계학습 기반의 시스템에서 매우 중요한 과정이다. 이미 정의된 피처를 바꾸어 새로 정의하거나 새로운 피처를 추가해 나감으로써 피처벡터들의 내적(또는 사이각)값이 상품간의 유사성을 좀더 더 잘 표현할 수 있도록 하여 추천 시스템을 개선해 나간다.

피처8, 피처9, 피처10에 대해 간단히 언급하고 넘어가자. 이 피처들은 분류기호를 이용해 만들었는데, 여기서 분류기호는 사용할 수 있는 값이 미리 정해져 있는, 즉 범주형(categorical) 정보에 속한다. 어떤 도서이던지 정해진 분류기호가운데 한 가지를 갖게 된다는 뜻이다. 도서의 분류기호에 다양한 종류가 있지만, 편의상 문학, 역사, 자연과학의 3종류만 있다고 해 보자. 피처8, 피처9, 피처10를 보면 분류기호가 문학인 경우 (1, 0, 0), 분류기호가 역사인 경우 (0, 1, 0), 분류기호가 자연과학인 경우 (0, 0, 1) 인 것을 알 수 있다. 범주형 정보를 허용되는 값의 경우의 수(cardinality) 과 같은 차원(dimension)의 벡터로 표현한 후 그 중 하나에만 1을 다른 모든 곳에는 0을 사용하는 방법을 원-핫(one-hot) 인코딩이라고 한다. 원-핫 인코딩은 범주형 정보를 피처로 변환하는 대표적인 방법이다.

이용정보의 이용

이용정보를 이용하는 방법이란 두 상품을 이용하거나 구매한 이용자 집단이 비슷한지를 평가하는 것이다. 예를 들어 좋은서점닷컴에서 서적1, 서적2, 서적3 을 구매한 사용자들이 다음과 같다고 해 보자.

구매한 사용자들
서적1사용자1, 사용자3, 사용자5, 사용자8, 사용자9
서적2사용자2, 사용자3, 사용자6, 사용자7
서적3사용자1, 사용자2, 사용자5, 사용자8, 사용자9, 사용자10

간단한 방법으로 자카드 유사성(Jaccard similarity)을 생각할 수 있다. 자카드 유사성은 각 상품의 구매자들을 집합으로 표현한 후, 두 집합의 교집합(intersection)의 크기를 합집합(union)으로 나눈 것이다. 예제의 서적들을 대상으로 계산해 보면 다음과 같다.

f자카드_유사성(서적1,서적2)=사용자3사용자1,사용자2,사용자3,사용자5,사용자6,사용자7,사용자8,사용자9=18=0.125f자카드_유사성(서적1,서적3)=사용자1,사용자5,사용자8사용자1,사용자2,사용자3,사용자5,사용자8,사용자9,사용자10=37=0.428\begin{align*} f_{자카드\_유사성}(서적1, 서적2) &= \frac{ \left| { 사용자3 } \right| }{ \left| {사용자1, 사용자2, 사용자3, 사용자5, 사용자6, 사용자7, 사용자8, 사용자9} \right| } \\ &= \frac{1}{8} = 0.125 \\ f_{자카드\_유사성}(서적1, 서적3) &= \frac{ \left| { 사용자1, 사용자5, 사용자8} \right| }{ \left| { 사용자1, 사용자2, 사용자3, 사용자5, 사용자8, 사용자9, 사용자10} \right| } \\ &= \frac{3}{7} = 0.428 \\ \end{align*}

점별 상호정보량(pointwise mutual information) 방법도 많이 쓰이는 방법이다. 점별 상호 정보량의 수학적인 정의는

log2상품 X와 Y를 동시에 구매하는 확률상품 X를 구매하는 확률×상품 Y를 구매하는 확률\log_{2} \frac{상품\ X와\ Y를\ 동시에\ 구매하는\ 확률}{ 상품\ X를\ 구매하는\ 확률 \times 상품\ Y를\ 구매하는\ 확률}

이다. 동일한 사용자가 두 상품을 구매할 확률이 각 상품의 구매가 확률적으로 독립적인 경우에 대비하여 얼마나 높거나 낮은지를 표현한다. 예제의 서적들을 대상으로 계산해 보자.

서적1을 구매하는 확률=5/10=0.5서적2를 구매하는 확률=4/10=0.4서적3을 구매하는 확률=6/10=0.6서적1과 서적2를 모두 구매하는 확률=1/10=0.1서적1과 서적3을 모두 구매하는 확률=3/10=0.3\begin{align*} 서적1을\ 구매하는\ 확률 &= 5 / 10 = 0.5 \\ 서적2를\ 구매하는\ 확률 &= 4 / 10 = 0.4 \\ 서적3을\ 구매하는\ 확률 &= 6 / 10 = 0.6 \\ 서적1과\ 서적2를\ 모두\ 구매하는\ 확률 &= 1 / 10 = 0.1 \\ 서적1과\ 서적3을\ 모두\ 구매하는\ 확률 &= 3 / 10 = 0.3 \\ \end{align*}

이를 바탕으로 유사성을 계산해 보면 다음과 같다.

f점별_상호정보량_유사성(서적1,서적2)=log20.10.5×0.4=2.32f점별_상호정보량_유사성(서적1,서적3)=log20.30.5×0.6=3.32\begin{align*} f_{점별\_상호정보량\_유사성}(서적1, 서적2) &= \log_{2} \frac{0.1}{0.5 \times 0.4} = 2.32 \\ f_{점별\_상호정보량\_유사성}(서적1, 서적3) &= \log_{2} \frac{0.3}{0.5 \times 0.6}= 3.32 \\ \end{align*}

자카드 유사성 방법과 점별 상호 정보량 방법 모두에서 서적1과 서적3의 유사성이 서적1과 서적2의 유사성보다 높게 계산된 점을 알 수 있다.

두 방법 말고도 이용정보를 이용하여 유사성을 평가하는 다른 방법들도 있다. 행렬 분해(matrix factorization)에 대해 이후의 글에서 설명하게 될 텐데, 이 또한 유사상품 추천을 위해 사용이 가능하다.

유사상품 추천의 구현

추천 유사상품을 보여주는 페이지는 대표적으로 상품 상세정보 페이지이다. 가령 좋은서점닷컴에서 사용자가 서적1의 상세정보 페이지를 방문하면, 상세정보를 보여주고 나서 화면 하단에 서적1과 유사한 서적을 보여줄 수 있다.

추천 유사상품 계산은 미리 해 두는 것이 일반적이다. 판매 가능한 상품의 수가 N 이라고 하면, 하나의 상품의 유사상품을 찾기 위해 유사성 함수 ffN1N-1번 계산한다. 구현하는 방법에 따라 차이가 있을 수 있지만, 기본적으로 NN에 비례해서 시간이 걸리고, 이 계산을 사용자가 상세정보 페이지를 방문하는 순간에 하기에는 무리가 있다. 각 상품의 추천 유사상품을 kk개씩 찾는다고 할 때 N×kN \times k 개의 추천 유사상품을 미리 찾아 저장해 두고 필요할 때 그 결과를 이용한다.

속성정보와 이용정보를 이용하는 방법중 어떤 방법을 언제 사용할까? 어떤 상품에 대한 이용정보를 충분히 많이 확보하고 있는 경우라면 이용정보를 이용하는 방법이 보다 효과적인 경우가 많다. 하지만 이용정보가 충분히 많은 경우가 아니라면 주의해야 한다. 통계수치를 바탕으로 계산한 값은 그 수치에 사용된 표본의 크기가 작을 수록 통계 노이즈의 영향을 크게 받을 수 있기 때문이다. 이 글에서 서적1, 서적2, 서적3에 대해서 10명의 사용자 U1, …, U10 의 구매 정보를 예를 들어 설명했는데, 개념 설명을 위해서 사용한 것일 뿐 좋은서점닷컴의 전제 사용자가 10명 밖에 없다면 이용정보를 이용하는 방법은 적절치 않다.

두 가지 방법을 모두 구현하고 상황에 따라 선택하는 방법을 생각할 수도 있다. 새로 출시된 신상품의 경우 이용정보가 없을 수 밖에 없으므로 속성정보 방법을 사용할 수 밖에 없다. 시간이 지나서 상품에 대한 구매정보가 쌓일 때 까지는 속성정보를 사용하는 방법을 사용하고, 구매정보가 충분히 많아진 이후에는 이용정보를 사용하는 식으로 구현할 수도 있다.