이 글에서는

  1. 코사인 유사도에 대해서 이해해본다.
  2. 응용 사례로 음악 추천 사례를 소개한다.
  3. Tidyverse의 맥락에서 코드를 리팩토링해본다.

코사인 유사도란 무엇인가?

코사인 유사도가 무엇인지를 따지지 전에 먼저 따져야 할 것이 있다. 코사인 유사도를 구하려면, 해당 관찰값의 속성(feature)을 벡터로 표현해야 한다. 차원이 같은 두 개의 벡터가 있을 때 이 벡터들 사이의 각도의 코사인을 값이 코사인 유사도다.

코사인 값은 무엇을 의미할까? 두 벡터가 비슷한 방향을 향하고 있다면 1에 가까운 값이 된다. 자기 자신에 대한 코사인 유사도는 1이다. 반면 두 벡터가 큰 상관이 없다면 0이다. 반대의 지향을 갖고 있다면 -1에 가깝게 된다.





코사인 유사도는 두 벡터로 잴 수 있는 가장 기본적인 지표인 (유클리드) ’거리’와 어떻게 다를까? 거리의 경우는 둘 사이가 얼마나 떨어져 있는지를 측정한다. 2차원 벡터를 생각해보자. \((10, 10)\)\((100,100)\)이 있다고 하자. 두 벡터는 같은 지향을 지니고 있기 때문에 코사인 유사도는 1이다. 반면, 거리는 \(\sqrt{(100-10)^2 + (100-10)^2}\)이다. 즉, 코사인 유사도는 해당 벡터의 절대 위치보다는 어느 방향을 향하고 있는지를 측정한다.





닷프로덕트의 정의를 활용하면 \(\cos \theta\)를 다음과 같이 정의할 수 있다. 차원에 동일한 두 개의 벡터 \(\mathbf a\), \(\mathbf b\)가 있다고 하자.

\[\begin{align} \cos \theta = \dfrac{\mathbf a \cdot \mathbf b}{\lVert \mathbf a \rVert \lVert \mathbf b \rVert} \end{align}\]

요약하면 두 벡터의 길이(norm)와 닷프로덕트만 계산하면 코사인 값을 얻을 수 있다.

Song Data

코사인 유사도를 살필 수 있는 좋은 사례가 없을까, 찾다가 좋은 튜토리얼을 발견했다. 이 튜토리얼은 사용자별 플레이 기록을 통해 추천곡 목록을 생성하는 내용을 담고 있다. 여기서 추천곡이란 어떤 특정한 노래와 비슷한 노래들을 지칭한다. 비슷함의 정의를 코사인 유사도로 두는 것이 핵심이다. 앞서 말했지만 코사인 유사도를 계산하라면 관심 대상을 벡터로 나타내야 한다. 튜토리얼에서는 다음과 같이 벡터를 생성했다.

\[\begin{align} s_i = [0, 0, \dotsc, \underset{\text{$u_j$의 청취횟수}}{5}, \dotsc] \end{align}\]

이용자의 집합 \(U = \left\{ u_1, u_2, \dotsc, u_n \right\}\)와 노래 집합 \(S = \left\{ s_1, s_2, \dotsc, s_m \right\}\)이 있다고 하자. \(U\)에 속하는 각 플레이어의 청취 횟수를 노래(\(s_j\)) 벡터로 만든다. 만일 해당 플레이어의 \(s_i\) 청취 횟수가 없다면 0이 될 것이다. \(S\) 벡터 공간의 차원은 \(m\)이다. 두 노래 벡터 \(s_i, s_j \in S\)가 주어지면 코사인 유사도를 쉽게 측정할 수 있다. 해당 튜토리얼에 이를 진행하는 R 코드가 있고 이를 반복 해설하지 않다. 이 글에서는 같은 코사인 유사도를 측정하는 다른 방식을 제시해보겠다.

Cosine Similarity in a Tidy Way

R의 데이터 다루기의 기본기로 자리잡아 가는 Tidyverse에 기반해서 코사인 유사도 계산을 다시 해보자. 우선, 위의 튜토리얼처럼 순진한 방식으로 계산을 해보기 바란다. 데이터를 다루는 관점에서 튜토리얼의 관점은 몇 가지 문제가 있다. 우선 노래가 매트릭스의 컬럼으로 가 있는데, 만일 노래를 1,000개에서 끊지 않는다면 매트릭스의 사이즈가 문제가 될 수 있다. 어차피 노래별로 그루핑해서 계산하는 것이라면 dplyr 패키지의 group_by 명령을 활용해 보다 쉽게 해결할 수 있지 않을까? 우선 이를 위해서 플레이 횟수가 많은 천곡의 리스트를 가져오겠다.

user song_id plays title artist_name
00003a4459f33b92906be11abe0e93efc423c0ff SOVMGXI12AF72A80B0 1 Hey Mama Black Eyed Peas
00003a4459f33b92906be11abe0e93efc423c0ff SOWVBDQ12A8C13503D 3 Volveré Gian Marco
00030033e3a2f904a48ec1dd53019c9969b6ef1f SOBONKR12A58A7A7E0 2 You’re The One Dwight Yoakam
00030033e3a2f904a48ec1dd53019c9969b6ef1f SONTQUO12A6D4F7D8B 4 Chasing Cars Snow Patrol
00030033e3a2f904a48ec1dd53019c9969b6ef1f SONYKOW12AB01849C9 7 Secrets OneRepublic
00030033e3a2f904a48ec1dd53019c9969b6ef1f SOXAOAJ12AB0184578 3 21 Guns [feat. Green Day & The Cast Of American Idiot] (Album Version) Green Day

보는 바와 같이 우리가 이용할 데이터는 일종의 key에 해당하는 user, song_id 그리고 해당 곡의 플레이 횟수, 이름, 아티스트의 제목으로 이루어져 있다. 코사인 유사도 계산을 위한 전략은 세가지로 나뉜다.

  1. 두 개의 노래 벡터 \(s_i\), \(s_j\)가 주어졌을 떄 코사인 유사도를 계산
  2. 위 데이터를 이용자별 노래별로 그룹핑
  3. 하나의 타켓 노래와 나머지 노래 모두에 대해서 코사인 유사도 계산

코사인 유사도 계산

이를 계산하는 함수 calc_cos_sim는 다음과 같다.

두 개의 벡터를 받아서 해당 각각 벡터의 길이와 닷프로덕트를 계산하고 이를 통해 코사인 유사도를 계산하는 함수다. 주의해야 할 점이 있다. 앞서 말했듯이 코사인 유사도 계산에 들어가는 두 벡터의 차원이 동일해야 한다. 벡터의 길이는 대상 이용자 전체의 숫자가 되어야 한다. 그런데, 위의 코드를 보면 전체 이용자 보다는 닷 프로덕트를 구하는 두 개의 벡터 중 차원이 큰 것이 맞춰지게 되어 있다. 이는 계산 상의 편의를 위한 것이다. 어차피 비교가 되는 두 벡터 모두에서 제외된 차원은 벡터 값이 0이 되므로 곱해도 0이 된다. 편리한 계산을 위한 트릭 정도로 이해하면 좋겠다.

만일 \(s_i\)에 속한 이용자와 \(s_j\)에 속한 이용자가 다르다면 둘의 합집합을 취해야 한다. 이때 각각의 노래에 속하지 않은 이용자의 재생 횟수는 0으로 처리하면 된다. 위 코드에서 full_joinreplace_na가 이를 처리해주고 있다.

그루핑을 통한 코사인 유사도 계산

이 처리는 함수 generate_song_list_by_cos_sim 내부에서 수행된다. 즉, 적당한 tibble과 타겟 song_id가 주어져 있을 때 song_id 별로 그루핑을 한 뒤 이 그룹에 대해서 위에서 정의한 calc_cos_sim을 일괄적으로 적용한다.

사례: Nirvana and U2

cos_sim title artist_name
1.0000000 Come As You Are Nirvana
0.3903533 The Man Who Sold The World Nirvana
0.3568732 Lithium Nirvana
0.1958162 Heart Shaped Box Nirvana
0.1186160 Behind Blue Eyes Limp Bizkit
0.0952245 Fakty Horkyze Slyze
0.0851531 In My Place Coldplay
0.0763979 When You Were Young The Killers
0.0692723 The Middle Jimmy Eat World
0.0667578 Flashing Lights Kanye West

코사인 유사도가 제대로 계산되고 있는지 확인해볼 요량으로 일부러 타겟 노래를 빼지 않았다. 자기 자신에 대한 유사도는 1로 가장 높은 값이 나온다. 대체로 너바나의 곡들이 상위에 있음을 알 수 있다.

cos_sim title artist_name
1.0000000 With Or Without You U2
0.1428010 Blood El-P
0.1180037 Alejate De Mi Camila
0.0670201 Esta Es Para Hacerte Féliz Jorge Gonzalez
0.0508873 More Than Words Extreme
0.0487711 Un Dia Gris Paulina Rubio
0.0483759 Mamma Bram Vermeulen
0.0483152 Hey Daddy (Daddy’s Home) Usher
0.0458394 Marry Me Train
0.0445650 No One Alicia Keys

U2의 “With or Without You”와 비슷한 곡을 나열해보았다. 일단 코사인 유사도가 인정할 만큼 높은 노래가 없다. 그나마 비슷하게 선택된 노래들 역시 그리 비슷하지 않은 듯 싶다. 역시 U2의 이 노래는 위대한 곡이었다!