본문 바로가기
데이터 분석/데이터 분석

개인화 추천 알고리즘 6 : Word2Vec (CBOW, Skip Gram)

by thomasito 2022. 2. 27.
반응형

벡터를 모르면 추천 알고리즘을 이해할 수 없다

 우리 생활속에 수 많은 추천 알고리즘이 있다. 넷플릭스, 유튜브, 쿠팡 등 어느 곳에 가도 안물 안궁이지만 추천을 해준다. 아마 내가 본 것과 비슷한 것들을 계속 추천해주는 것을 대강은 알 수 있는데 이것은 대부분의 추천 알고리즘이 유사도(Similarity)를 기반으로 하고 있기 때문이다. 그리고 알고리즘 내부는 수 많은 벡터로 이루어져 있다. 유튜브의 과거 시청 이력도 스포티파이의 과거 음악 청취 이력도 모두 벡터화되어 유사도를 계산한다.

 

 스포티파이 같은 경우 수 많은 고객데이터(=고객 벡터)와 음악청취데이터(=음악벡터)를 가지고 있다. 고객 1의 음악리스트와 가장 유사한 고객을 뽑아서 그 고객의 플레이 리스트에 있는 음악과 가장 유사도가 높은 것을 추천해준다. 

Word2Vec : 단어를 벡터로 만들기

 Word2Vec 이라고 특별한 게 없다. 단어를 벡터로 만들 뿐이다. Word2Vec 같은 경우는 가장 유명한 사례를 아래 페이지에서 찾을 수 있다.

https://word2vec.kr/

 

Korean Word2Vec

ABOUT 이곳은 단어의 효율적인 의미 추정 기법(Word2Vec 알고리즘)을 우리말에 적용해 본 실험 공간입니다. Word2Vec 알고리즘은 인공 신경망을 생성해 각각의 한국어 형태소를 1,000차원의 벡터 스페이

word2vec.kr

 

 예를 들면 이런 식이다. 남자와 왕자의 벡터를 더한 후 여자의 벡터를 빼면? 바로 공주의 벡터가 나온다. 각 단어간의 벡터 유사도를 바탕으로 특정 단어들이 있을 때 어떤 단어가 가장 어울리는지 추천해주는 것이다. (아마 내부에 수많은 데이터를 바탕으로 학습된 단어들에게 벡터값이 부여되어 있을 것이다.)

 

CBOW, Skip Gram

 Word2Vec은 기본적으로 '비슷한 문맥에서 쓰이는 단어들은 비슷한 의미를 가진다'라는 사실을 기반으로 하고 있다. CBOW는 Continous Bag of Words의 약자로 주변단어를 통해 중심단어를 예측하는 것이다. 반면에 Skip Gram은 중심단어를 통해 주변 단어를 예측한다. 예시를 들어서 보면 간단한데

  • 나는 우울할 때 힙합을 듣지. -> 이런 데이터를 학습하고 중심단어를 '힙합을' 이라고 가정하자.
  • (CBOW) 나는 우울할 때 _____ 듣지. -> 중심단어인 '힙합을' 예측한다.
  • (Skip Gram) 나는 _______ 힙합을 _____. -> 주변단어인 '우울할 때'와 '듣지'를 예측한다.

 

 어떤 머신러닝 기법이든 그러겠지만 인풋과 아웃풋은 엄청 심플하게 나오지만 그 내부에서 치고박고 돌아가는 알고리즘은 복잡하다. CBOW, Skip Gram 같은 경우는 인공신경망 모델을 활용한다. 쉽게 말하면 임의의 가중치를 부여해서 데이터 예측값을 내놓고 실제 결과값과 계속 비교해가면서 가중치를 조정해나가는 방법이다. 출력층에 나타난 결과값을 소프트맥스(Softmax) 함수를 통해 확률화 한 다음에 실제 레이블과 비교한다.

 

 

CBOW 사례

 CBOW, Skip Gram에서 국룰처럼 사용되는 비틀즈의 노래 "You say goodbye and i say hello"를 통해 살펴보겠다. You는 [1,0,0,0,0,0,0] 벡터, say는 [0,1,0,0,0,0,0] 벡터를 쭉 만들어준다. 우선 처음의 가중치는 랜덤으로 넣어주기 때문에 W_in 이라는 변수에 np.random.randn(7,3) 으로 랜덤한 (7 X 3) 행렬을 생성해준다.

import numpy as np
# 입력값은 원-핫 벡터형태를 가짐

input1 = np.array([1,0,0,0,0,0,0]) # You
input2 = np.array([0,0,1,0,0,0,0]) # goodbye

# (입력 X 차원의 크기) - 차원의 크기는 사용자가 선정
# 초기의 Weight 는 랜덤한 값으로 정해짐
W_in = np.random.randn(7,3)

W_in

 

 은닉층의 h 값을 계산해준다. You 벡터값과 W_in 을 행렬곱해 h_1 값을 구하고, goodbye 벡터값과 W_in 을 행렬곱해 h_2 를 구한후 평균값을 구해준다. (h = (h_1 + h_2) /2)

h_1 = np.matmul(input1, W_in)
h_2 = np.matmul(input2, W_in)

print(h_1+h_2/2)
h = (h_1 + h_2)/2

 W_in 과 마찬가지고 W_out 이라는 변수에 (3  X 7)  무작위 행렬을 생성한 후, h와 W_out 행렬곱을 계산한다. 하지만 이렇게 점수화된 벡터를 정답레이블과 비교할 수 없으므로 소프트맥스 함수를 통해 확률화 시키는 과정이 필요하다. 이 과정을 통해 score라는 벡터가 생성된다.

W_out = np.random.randn(3,7)
score = np.matmul(h, W_out)
print(np.round(score,4))

 소프트맥스 함수는 각 변수를 Exponential 화하여 확률을 구하는 함수이다. pred 변수에 들어간 값을 보면 이렇게 해석할 수 있다. 주변 단어가 you, say인 경우에 중심단어들이 나올 확률을 보면, you가 0.15, say가 0.41, hello가 0.25로 죽 나온다. (score 벡터의 모든 값을 더하면 1이다.)

def softmax(x):
  exp_x = np.exp(x)
  # exponentiial e^x 로 만든 후
  sum_exp_x = np.sum(exp_x)
  # exponential 을 모두 더해줌
  y = exp_x / sum_exp_x
  # sum_exp_x = 1 에서 exp_x가 차지하는 비중을 구함
  return y
  
  pred = softmax(score)
  
  print(np.round(pred,4))
# 각 단어가 나올 확률을 계산

 

 그리고 정답 레이블인 [0,1,0,0,0,0,0,0] 과 비교하여 에러 값을 계산해준다. 크로스 엔트로피 값은 0.12로 나왔다. 

# Cross Entropy Loss를 계산
def cross_entropy_error(y,t):
  '''
  y : prediction
  t : target
  '''
  delta = 1e-7 # log의 내부가 0이 되는 것을 방지

  # y.shape[0] 으로 나눠주는 이유는 배치 사이즈 반영
  print(y)
  print(y.shape)
  print(y.shape[0])
  print(t)

  return -np.sum(t * np.log(y+delta) / y.shape[0])
  
  cross_entropy_error(pred,[0,1,0,0,0,0,0])

 

# Loss를 통해서 Weight 를 업데이트
# Softmax의 미분값
ans = [0,1,0,0,0,0,0]
ds = np.round(pred - ans, 4)
print(ds)

# ds (delta for W_out) 계산
dW_out = np.outer(h, ds)
print(np.round(dW_out, 4))

 은닉층 h 벡터와 소프트맥스 함수의 확률과 정답 레이블의 차이인 ds 벡터를 외적(outer product)해준다.  

 

 ds 1 X 7 벡터과 W_out 행렬을 전치(transpose)한 7 X 3 행렬을 연산하므로 결과는 da 1 X 3 벡터일 것이다.  You에 해당하는 벡터와 da를 2로 나눈 값을 외적하고, hello에 해당하는 벡터와 da를 2로 나눈 값을 외적하여 dw_1, dw_2 를 계산한다.

da = np.dot(ds, W_out.T)
print(da)

dw_1 = np.round(np.outer(np.array([[1,0,0,0,0,0,0]]), (da/2)), 4)
dw_2 = np.round(np.outer(np.array([[0,0,1,0,0,0,0]]), (da/2)), 4)

dw_1
dw_2

 

  Learning rate 를 1로 가정하고 dw_1 만큼 W_in_new 새로운 가중치에 반영해준다. Learning rate는 학습률이라고도 하며 이 값이 1인 것은 dw_1 행렬의 데이터(=가중치)를 전부 새로운 W_in_new에 반영한다는 의미이다.  W_out도 마찬가지로 Learnig rate를 1로 가정하고 W_out_new 새로운 가중치를 반영한다. 결과적으로 W_in, W_out 은 처음의 값에서 가중치를 반영한 값으로 변경되었다. (이렇게 정답레이블과 비교하여 계속 가중치를 변경하는 것이 인공신경망의 기본 원리이다.)

learning_rate = 1

W_in_new = W_in- learning_rate * dw_1
W_in_new = W_in_new - learning_rate * dw_2
print(np.round(W_in_new,4))
# 수정된 W_in

learnoutg_rate = 1
W_out_new = W_out - learnoutg_rate * dW_out
print(np.round(W_out_new, 4))
# 수정된 W_out

 

 이렇게 새로운 W_in_new, W_out_new 변수를 입력값으로 다시 구하면 say가 중심단어로 나올 확률 73%로 개선되었다. 

h_1 = np.matmul(input1, W_in_new)
h_2 = np.matmul(input2, W_in_new)
h = (h_1+h_2) / 2

score = np.matmul(h, W_out_new)
print(np.round(score,4))

pred = softmax(score)
print(np.round(pred,4))
# 각 단어가 나올 확률을 계산

 

가중치를 계속 계속 반영하는 것이 핵심

 이게 내부 알고리즘은 엄청 복잡하다. 그런데 중요한 것은 예측값과 실제값을 비교하면서 가중치를 계속 조정해나가고 가장 가까운 예측값을 찾아나가는 과정이라고 보면 된다. Skim Gram은 CBOW와 달리 중심단어를 통해 주변단어를 예측하는 과정이기 때문에 은닉층의 평균을 구하는 과정이 필요없다. 결국 도출된 결과를 가지고 얼마나 잘 해석할 수 있는지가 중요하겠다.

 

 아래 위키독스의 도움을 많이 받았다. ^^ (능력자 분들이 정말 많다.)

https://wikidocs.net/22660

 

02) 워드투벡터(Word2Vec)

앞서 원-핫 벡터는 단어 벡터 간 유의미한 유사도를 계산할 수 없다는 단점이 있음을 언급한 적이 있습니다. 그래서 단어 벡터 간 유의미한 유사도를 반영할 수 있도록 단어의 ...

wikidocs.net

 

반응형

댓글