인공지능

Word2Vec

NickTop 2024. 5. 1. 23:25

word2vec은 단어를 숫자로 변환하는 방법 중 하나입니다. word2vec 이전에 썼던 방법은 one-hot encoding이 있습니다

One-hot encoding

다음과 같은 문장을 봅시다

 

Laughter echoed through the hall as the children played.

각 단어 당 차원 1개를 할당하여 매핑합니다

  0 1 2 3 4 5 6 7 8
Laughter 1                
echoed    1              
through      1            
the        1          
hall          1        
as           1      
the              1    
children                1  
played                 1

예를들어 echoed를 vector로 나타내면 [0,1,0,0,0,0,0,0] 입니다

 

다음과 같은 단점이 있습니다

1. 단어 1개당 차원 1개가 필요해 차원이 크게 늘어난다. (그마저도 벡터의 대부분의 값은 0이다)

2. Orthogonality : 각 단어의 내적이 0임. 단어간의 의미를 파악할 수 없다

 

Word2Vec

"비슷한 문맥에 있는 단어들은 비슷한 뜻을 가지고 있다"

...President Moon said yesterday...

...President Trump said yesterday...

...President Putin said yesterday...

각 3단어들은 비슷한 문맥에서 쓰였으므로 비슷한 단어라는 것을 알 수 있다.

=> 주변단어와 중심단어로부터 임베딩을 학습하자!

 

주변단어 : President, said, yesterday

중심단어 : Moon

주변단어로부터 중심단어 예측하는 방법 => CBOW(Continuous Bag of Words)

중심단어로부터 주변단어 예측 하는 방법 => Skip-gram

Window : 주변단어 앞뒤로 몇개의 글자를 주변단어로 가져올 것인가

 

CBOW

cbow 도식화 이미지

Vfat은 fat을 word2vec로 구한 결과임

projection layer를 구하기 위한 weight, output layer를 구하기위한 weight, 총 2개의 weight가 필요함

loss는 크로스 엔트로피 함수를 쓰면 됨

Bias는 없다

Skip-gram

skip gram 도식화 이미지

 

CBOW : 학습이 빠름

Skip-gram : 적게 등장하는 단어들도 학습에 여러번 쓰이기 때문에 잘 임베딩 됨

Skip-gram이 일반적으로 성능이 더 좋다고 한다.

 

 

실제로 쓰이지 않음 => Negative sampling

 

코드

딥러닝 라이브러리 없이 skip gram을 구현해보겠습니다

데이터는 책 데이터 뒤져보다가 txt로만 된 것이 있어서 gutenberg에서 가져왔습니다.

import requests
URL = 'https://www.gutenberg.org/cache/epub/73510/pg73510.txt'
response = requests.get(URL)
response.status_code
txt = response.text

 

전처리는 그냥 nltk를 썼습니다, 저는 통으로 학습했는데, 문장 단위로 끊어서 학습하는 것이 더 좋습니다.

import nltk
nltk.download('punkt')
from nltk.tokenize import word_tokenize

def parse_text(txt):
  ascii_letters = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ')
  txts = txt.split('\n')

  START = False
  END = False
  words = []
  for t in txts:
    t = t.replace('\r','')
    t = t.replace('_',' ')
    t = t.replace('-',' ')
    if t == 'THE CASTAWAYS':
      START = True
    if t == 'THE END':
      END = True
    if START and not END:
      for word in word_tokenize(t):
        if set(word) & set(ascii_letters):
          words.append(word)
  words = list(map(lambda x: x.lower(), words))
  return words

def make_one_hot_encoder(words):
  one_hot_encoder = {}
  v = len(set(words))
  for word in words:
    if word not in one_hot_encoder:
      one_hot_encoder[word] = np.zeros(v)
      one_hot_encoder[word][len(one_hot_encoder)-1] = 1
  return one_hot_encoder

words = parse_text(txt)
one_hot_encoder = make_one_hot_encoder(words)

 

back propagation은 softmax + cross entropy loss를 같이 썼을 때 계산을 쉽게 할 수 있어 간단하게 구현할 수 있습니다

https://towardsdatascience.com/derivative-of-the-softmax-function-and-the-categorical-cross-entropy-loss-ffceefc081d1

import numpy as np

def forward(Wvm, Wmv, idx) : # idx: 중심단어  
  P = Wvm @ one_hot_encoder[words[idx]].T # projection layer
  O = Wmv @ P # output layer
  S = np.exp(O) / np.sum(np.exp(O))
  return P, O, S

def backward(Wvm, Wmv, P,O,S, idx):
  dO = np.zeros(len(one_hot_encoder))
  for i in range(idx-window_size,idx+window_size+1,1):
    if i == idx:
      continue
    if i < 0 or i >= len(words):
      continue
    dO += S - one_hot_encoder[words[i]]
  
  dWmv = np.outer(dO, P)
  dP = dO @ Wmv
  dWvm = np.outer(dP, one_hot_encoder[words[idx]])

  Wvm -= learning_rate * dWvm
  Wmv -= learning_rate * dWmv
  return Wvm, Wmv
def train():
  global Wvm, Wmv
  for e in range(epoch):
    print(f"epoch:{e}")
    for i in range(len(words)):
      P,O,S = forward(Wvm, Wmv, i)
      Wvm, Wmv = backward(Wvm, Wmv, P,O,S, i)
# hyper-parameters
window_size = 2
vector_size = 100
learning_rate = 0.01
epoch = 3

Wvm = np.random.rand(vector_size, len(one_hot_encoder))
Wmv = np.random.rand(len(one_hot_encoder), vector_size)

train()

 

학습이 잘 되었는지 확인해봅시다

유사도가 가장 높은 단어들을 살펴봅시다

word2vec = {}
word_set = set()
for word in one_hot_encoder:
  word2vec[word] = Wvm @ one_hot_encoder[word]
  word_set.add(word)

def cosine_similarity(a, b):
  return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

def find_top_two_similar_words(word2vec):
  word_list = list(word_set)
  sim_list = []
  for i in range(len(word_list)):
    for j in range(i+1, len(word_list)):
      word1 = word[i]
      word2 = word[j]
      sim_list.append((word1, word2, cosine_similarity(word2vec[word1], word2vec[word2])))
  return sorted(sim_list, key=lambda x: -x[2])
sim_list = find_top_two_similar_words(word2vec)
print(sim_list[:10])

 

[('ye', 'you', 0.934766170577427),
 ('he', 'they', 0.9267597479993857),
 ('for', 'with', 0.9233073761163063),
 ('was', 'is', 0.9221814438976493),
 ('him', 'them', 0.9183951146136528),
 ('and', 'but', 0.9139173115845995),
 ('in', 'into', 0.9131892792371982),
 ('we', 'you', 0.9118327913774454),
 ('i', 'we', 0.9108868958041368),
 ('by', 'in', 0.9099312696120511)]

 

 

최적화를 하려면

1. negative sampling

2. hierarchical softmax

와 같은 기법이 있습니다.

 

https://wikidocs.net/22660