오늘은 영화 시놉시스를 바탕으로 영화 추천 알고리즘을 만들어 보려고 한다.
내가 "베테랑"이라는 영화를 좋아했다고 할때, 이와 가장 유사한 영화순으로 추천을 해 줄것이다.
필요한 절차는 다음과 같다.
(1) 영화 시놉시스에서 주요 단어를 추출하기
(2) 불용어(의미없는 단어) 제거하기
(3) TF-IDF를 구하여 문서(영화)별로 어떤 단어가 중요하게 사용되었는지 구하기
(4) 각 문서마다 "베테랑"과의 코사인 유사도를 계산하여 유사도가 높은 순으로 결과 정렬하기
첨부파일
영화 시놉시스에서 주요 단어를 추출하기
시놉시스는 네이버에서 대충 긁어왔다. 첨부파일 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 구하기
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)
나는 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이다.
'프로그래밍 > Python' 카테고리의 다른 글
Python 정규식 그룹명으로 match 결과 가져오기 (group name) (0) | 2022.05.07 |
---|---|
Falcon req.get_param()으로 모든 파라미터를 받아보자 (0) | 2022.04.06 |
Python 프로젝트 패키지 관리하기: pip freeze requirements.txt (0) | 2021.09.18 |
Python 배열 슬라이싱 공략 (Numpy) (0) | 2021.03.07 |
[Python] 마방진 그리기 / 검산하기 with numpy (0) | 2021.02.12 |