이전 포스트 Content-based 추천 시스템 구현 (1)에서 Content-based 방식의 추천 알고리즘에대해 알아보고 간단하게 구현해보았다. 이때 상품의 카테고리를 기준으로 one-hot encoding한 벡터값을 활용하여 코사인 유사도를 계산하였는데, 이러한 방식은 단순히 많이 구매한 카테고리에 속한 상품을 추천해주는 것으로 밖에 안된다. 이러면 굳이 계산할 필요없이 카테고리 별로 구매한 횟수만 저장해놓고 가장 많이 구매한 카테고리의 상품들을 추천해줄 수 있다. 또한 가장 많이 구매한 카테고리에만 편향(Bias)되어 다른 카테고리에 속한 상품은 추천 대상에서 제외되는 문제가 발생하게 된다.
상품 프로필 구성
위와 같은 문제를 해결하기위해 카테고리 뿐만 아니라 상품 등록시 작성한 제목과 본문도 활용해볼 수 있을 것 같다는 생각을 했다.
카테고리의 경우 개수가 정해져있기 때문에 간단히 one-hot encoding을 할 수 있지만, 비정형 데이터인 제목과 본문 텍스트는 어떻게 가공해줄 수 있을까?
나는 바로 오픈소스 Huggingface
의 transformers
라이브러리에서 제공되는 pre-trained tokenizer를 떠올릴 수 있었다. 한국어를 지원하는 pre-trained tokenizer 중 단순하게 다운로드 횟수가 많은 FacebookAI/xlm-roberta-base를 선택했다. (100개의 언어를 지원한다.)
이제 Tokenizer가 무엇을 해줄 수 있는지 살펴보자.
Tokenizer는 텍스트 문장을 미리 나누어 놓은 Token 단위로 나누어주거나 id 값으로 표현해준다. 예를 들어 다음과 같이 수행할 수 있다.
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("xlm-roberta-base")
print(tokenizer.tokenize("노트북 구매하면서 필요가 없어졌네요"))
print(tokenizer.encode("노트북 구매하면서 필요가 없어졌네요"))
"""
['▁노트북', '▁구매', '하면서', '▁필요가', '▁없어', '졌', '네요']
[0, 236477, 39010, 30804, 135385, 77977, 80955, 25861, 2]
"""
tokenize()
함수를 호출하면 token 단위로 나누고, encode()
를 호출하면 id 값으로 표현해준다. id 값으로 표현할 때 처음(0)과 마지막(2) 값은 special token이라고 불리며 처음과 끝을 의미하는 용도로 사용된다.
여기서 사용된 tokenizer가 나눈 token의 개수(vocab_size)는 250002이다. 그리고 이 크기만큼 벡터 길이를 지정하여 상품 프로필을 구성하는데 활용할 것이다. 상당히 크기가 크다고 생각이 들지만 빅데이터 시대라 불리는 요즘, 그리고 상당히 많이 발전한 하드웨어 기술로 어느 정도 수용할 수 있지 않을까 싶다. 크기가 큰 것으로 인해 문제가 된다면 더 적은 tokenizer를 탐색해볼 수 있을 것 같다. (많은 종류의 언어가 포함되어 있어서 그런 것 같다.)
이렇게 tokenzier를 활용하여 제목과 본문 텍스트를 하나의 벡터로 만들고, 그 뒤에 카테고리에 대한 벡터를 이어주도록 했다. 바로 코드로 작성해보자.
# id 값으로 표현된 리스트 반환
def encode(content: str):
return tokenizer.encode(content, return_tensors='np')
# 카테고리 수
num_of_category = 11
# (vocab_size + 카테고리 수) 길이만큼의 벡터를 정의
post_profile = np.array([0.] * (tokenizer.vocab_size + num_of_category))
post = "노트북 구매하면서 필요가 없어졌네요"
category = 3
# id 값을 인덱스로 사용하여 one-hot encoding
post_profile[encode(post)] += 1
# 카테고리에 해당하는 위치에도 값 1 지정
post_profile[tokenizer.vocab_size + category] += 1
print(post_profile)
"""
[1. 0. 1. ... 0. 0. 1.]
"""
이로써 상품 프로필 구성이 끝났다. 하나의 상품 프로필의 벡터 길이는 250013(250002 + 11)이 된다.
사용자 프로필 구성
사용자 프로필도 상품 프로필을 이용하는 것이므로 기본적인 로직은 큰 차이가 없다.
def encode(content: str):
return tokenizer.encode(content, return_tensors='np')
num_of_category = 11
user_profile = np.array([0.] * (tokenizer.vocab_size + num_of_category))
# 구매한 내역
buyed1 = "노트북 구매하면서 필요가 없어졌네요"
category1 = 11
user_profile[encode(buyed1)] += 1
user_profile[tokenizer.vocab_size + category1] += 1
buyed2 = "그래픽 1080이고 씨피유는 i5 7500 ssd 256g 메인보드 b250 바주카 게이밍"
category2 = 11
user_profile[encode(buyed2)] += 1
user_profile[tokenizer.vocab_size + category2] += 1
buyed3 = "사양 및 구매 일자는 사진에 포함되어 있습니다."
category3 = 11
user_profile[encode(buyed3)] += 1
user_profile[tokenizer.vocab_size + category3] += 1
# 평균
user_profile /= 3
print(user_profile)
"""
[1. 0. 1. ... 0. 0. 0.33333333]
"""
코사인 유사도 계산
여기서부터는 이전 포스트와 다르지 않다.
from numpy import dot
from numpy.linalg import norm
def cos_sim(A, B):
return dot(A, B)/(norm(A)*norm(B))
print(cos_sim(user_profile, post_profile).item())
"""
0.588348405414552
"""
이렇게 코사인 유사도를 계산해주고 상위에 지정한 개수만큼 추천해주면 된다.
다음 포스트에서는 실제 FastAPI를 사용하여 추천 시스템 RESTAPI 서버를 구현해볼 것이다.