프로그래밍/Python

[Python] 코사인 유사도를 이용한 영화 추천 알고리즘 만들기

Lou Park 2021. 3. 12. 23:32

영화 제목과 시놉시스 일부

오늘은 영화 시놉시스를 바탕으로 영화 추천 알고리즘을 만들어 보려고 한다.

내가 "베테랑"이라는 영화를 좋아했다고 할때, 이와 가장 유사한 영화순으로 추천을 해 줄것이다.

필요한 절차는 다음과 같다.

 

(1) 영화 시놉시스에서 주요 단어를 추출하기

(2) 불용어(의미없는 단어) 제거하기

(3) TF-IDF를 구하여 문서(영화)별로 어떤 단어가 중요하게 사용되었는지 구하기

(4) 각 문서마다 "베테랑"과의 코사인 유사도를 계산하여 유사도가 높은 순으로 결과 정렬하기

 

첨부파일

전체 코드 + data.json 다운받기

 

 

영화 시놉시스에서 주요 단어를 추출하기

 

시놉시스는 네이버에서 대충 긁어왔다. 첨부파일 data.json 을 참고하면된다.

데이터의 제일 마지막에 비교하고자하는 영화 "베테랑"을 넣고 Konlpy의 형태소 분석기중에서 비교적 빠른편인 Okt를 이용해 명사(nouns)만 추출해낸다.

# target data는 이렇게 생겼다.
{
    'name': '베테랑',
    'content': "한 번 꽂힌 것은 무조건 끝을 보는 행동파 ‘서도철’(황정민), 20년 경력의 승부사 ‘오팀장’(오달수), 위장 전문 홍일점 ‘미스봉’(장윤주), 육체파 ‘왕형사’(오대환), "
               "막내 ‘윤형사’(김시후)까지 겁 없고, 못 잡는 것 없고, 봐 주는 것 없는 특수 강력사건 담당 광역수사대. 오랫동안 쫓던 대형 범죄를 해결한 후 숨을 돌리려는 찰나, "
               "서도철은 재벌 3세 ‘조태오’(유아인)를 만나게 된다. 세상 무서울 것 없는 안하무인의 조태오와 언제나 그의 곁을 지키는 오른팔 ‘최상무’(유해진). 서도철은 의문의 사건을 "
               "쫓던 중 그들이 사건의 배후에 있음을 직감한다. 건들면 다친다는 충고에도 불구하고 포기하지 않는 서도철의 집념에 판은 걷잡을 수 없이 커져가고 조태오는 이를 비웃기라도 하듯 "
               "유유히 포위망을 빠져 나가는데… 베테랑 광역수사대 VS 유아독존 재벌 3세 2015년 여름, 자존심을 건 한판 대결이 시작된다! "
}
# data를 불러온다.
with open('data.json') as file:
    data = json.load(file)

data.append(target_data)

# okt 토크나이저를 이용해 영화 시놉시스를 토큰화한다.
okt = Okt()
contents = list(map(lambda x: x['content'], data))
# 명사 단어들을 추출합니다.
vocab = []
for content in contents:
    vocab += okt.nouns(content)

 

 

 

 

불용어(의미없는 단어) 제거하기

예제 코드에서는 몇가지 stop_words를 정의 해 두었는데, 막상 vocab의 단어들을 보면 정말 의미없는 단어들이 많다. 흥~대충 몇가지 적다가 힘이 빠져서 그만뒀다. "검", "돈", "총" 등 한 글자 단어도 물론 의미가 있는게 있지만 "가", "다" 뭐 이런게 많을것 같아서 다 잘라버렸다.

# 중복을 제거합니다.
vocab = list(set(vocab))

# 불용어를 제거합니다. (stop_words에 포함되거나 1글자짜리는 제거)
stop_words = ['이제', '인물', '동안', '단번', '스무', '사이', '순간', '과연', '마저', '만큼', '누구', '주변', '소유자', '오늘']
vocab = list(filter(lambda x: len(x) > 1 and x not in stop_words, vocab))

 

 

 

 

TF-IDF 구하기

TFIDF란?

TF-IDF(w)는 TF와 IDF의 곱이다.

사진에서 볼 수 있는 것 처럼 어떤 문서 j의 단어 i에 대하여 tf(i, j)는 j에서 i의 등장빈도,

df(i)는 i 라는 단어가 등장하는 문서j 의 수,

N은 전체 문서의 수다.

# 특정문서 d에서의 특정단어 t의 등장 횟수
def tf(t, d):
    return d.count(t)

# df(t)에 반비례하는 수, df(t)=특정 단어 t가 등장한 문서의 수.
def idf(t, data):
    df = 0
    for d in data:
        df += t in d['content']
    return log(len(data) / (df + 1))

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

 

https://wikidocs.net/31698 에서 가져온 idf 식

나는 idf를 구할때 df(i)가 0일 경우를 피하기 위해 1을 더하는 식을 사용했다.

영화 데이터를 돌면서 한줄 한줄 계산한다.

# TF-IDF 구하기
result = []
for i in range(len(data)):
    result.append([])
    d = data[i]['content']
    for j in range(len(vocab)):
        t = vocab[j]
        result[-1].append(tfidf(t, d, data))

 

 

 

 

만들어진 TFIDF는 어떤 모습일까?

pandas를 이용해 이쁘게 TFIDF를 출력 해보려고 했는데...사용한 단어가 무려 604개! 불용어 제거를 제대로 못해서 아주 흉측한 Sparse vector의 모습이다. 실제로 사용한다면 의미있는 단어로 폭을 줄이는게 좋을 것 같다.

# 만들어진 TF-IDF DTM 출력
v = pd.DataFrame(result, columns=vocab, index=list(map(lambda x: x['name'], data)))
print(f'* 영화 수: {v.shape[0]}, 단어 수: ${v.shape[1]}')
print(v.to_string())

 

 

 

 

유사도가 높은 순으로 결과 정렬하기

cos_sim(비교 TFIDF, 비교 TFIDF)를 하게 되면 둘 사이의 유사도가 나온다. 코사인 유사도는 2개의 벡터값에서 코사인 각도를 구하는 방식이다. 두 벡터간 '거리'가아닌 '방향'을 나타낸다. -1 ~ 1의 값을 얻을 수 있는데, 1은 유사도가 가장 높고, 0은 관련 없음, -1은 유사하지 않다는 것이다.

# target_data와 코사인 유사도를 구해서 가장 높은 순으로 정렬
sim_scores = []
for i in range(len(data)):
    name = data[i]['name']
    if name != target_data['name']:
        sim_scores.append({
            'name': name,
            'score': cos_sim(v.loc[target_data['name']], v.loc[name])
        })

print('* 추천 순위')
sim_scores = sorted(sim_scores, key=lambda x: x['score'], reverse=True)
print(pd.DataFrame(sim_scores).to_string())

 

 

 

추천 결과 - 베테랑을 좋아하는 당신, 이 영화는 어때요!?

영화 <베테랑> - 어이가 없네? 유행어의 그 영화! 물론 난 안봤음.

18개의 모든 영화 데이터 중에서 베테랑과 가장 유사한 정도로 줄을 세워보았다.

한국 범죄영화인 아저씨, 신세계가 0.05점(....하ㅜ...)으로 다소 높게 나온게 얼추 맞게 나온거같아서 만족스럽다.

총 실행시간은 5.38s이다.

베테랑에 대한 영화 추천 결과. 어바웃 타임부터는 의미가 없군...