NLP

Word2Vec Practice

LYShin 2022. 12. 27. 23:40

- Word Embedding은 단어를 벡터로 표현하는 방법입니다. 이를 활용하면 단어와 단어 간의 관계를 벡터로 표현할 수 있게 됩니다.

- Word2Vec는 Word Embedding의 여러 방법중 하나입니다.

- 본 글에서는 CBOW와 Skip-gram을 활용하여 Word2Vec를 구현하겠습니다. 

- 실습에는 Colab을 사용합니다.

 

 

 

1. Import Package

 

이번 섹션에 필요한 패키지를 가져오고, 글꼴을 다운받고 설정합니다.

!sudo apt-get install -y fonts-nanum
!sudo fc-cache -fv
!rm ~/.cache/matplotlib -rf

from tqdm import tqdm
from konlpy.tag import Mecab,Twitter,Okt,Kkma
from torch import nn
from torch.nn import functional as F
from torch.utils.data import Dataset, DataLoader
from collections import defaultdict

import torch
import copy
import numpy as np

 

2. Text Processing

 

사용할 텍스트를 전처리합니다.

train_data = ['연습에 사용할 문장을 사용해주세요']
test_words = ['모델을 검정할 단어를 사용해주세요']

# Tokenizer로 Okt를 사용하겠습니다.
tokenizer = Okt()

# train_data를 token으로 만드는 함수를 만듭니다.
def make_tokenized(data):
  tokenized = []
  for sent in tqdm(data):
    tokens = tokenizer.morphs(sent, stem=True)
    tokenized.append(tokens)
  
  return tokenized

# train_data를 토큰화합니다.
train_tokenized = make_tokenized(train_data)

# 토큰화한 단어를 사용 개수 기준으로 정렬하겠습니다.
word_count = defaultdict(int)
for tokens in tqdm(train_tokenized):
  for token in tokens:
    word_count[token] += 1
word_count = sorted(word_count.items(), key=lambda x: x[1], reverse=True)
print(list(word_count))

# Word to Index를 만들겠습니다.
w2i = {}
for pair in tqdm(word_count):
  if pair[0] not in w2i:
    w2i[pair[0]] = len(w2i)
i2w={v:k for k,v in w2i.items()}

 

3. CBow & Skipgram

 

Cbow는 주변 단어를 이용해 주어진 단어를 예측하는 방법이고, Skipgram은 중심 단어를 이용하여 주변 단어를 예측하는 방법입니다. 입력데이터와 출력데이터를 설정하는 방법이 다르기 때문에 학습 방법에 맞게 Dataset를 구현합니다.

# Cbow의 방법대로 Dataset을 구성하는 class를 만들겠습니다.
class CBOWDataset(Dataset):
  def __init__(self, train_tokenized, window_size=2):
    self.x = [] # input word
    self.y = [] # target word

    # 토큰화한 학습데이터를 입력데이터와 출력데이터로 만드는 작업입니다.
    # window_size에 따라 중심 단어의 좌,우 총 2*window_size개씩 입력데이터에 넣고
    # 중심 단어를 출력데이터에 넣습니다.
    for tokens in tqdm(train_tokenized):
      token_ids = [w2i[token] for token in tokens]
      for i, id in enumerate(token_ids):
        if i-window_size >= 0 and i+window_size < len(token_ids):
          self.x.append(token_ids[i-window_size:i] + token_ids[i+1:i+window_size+1])
          self.y.append(id)

    self.x = torch.LongTensor(self.x)  # (전체 데이터 개수, 2 * window_size)
    self.y = torch.LongTensor(self.y)  # (전체 데이터 개수)

  def __len__(self):
    return self.x.shape[0]

  def __getitem__(self, idx):
    return self.x[idx], self.y[idx]
# Skipgram의 방법대로 Dataset을 구성하는 class를 만들겠습니다.
class SkipGramDataset(Dataset):
  def __init__(self, train_tokenized, window_size=2):
    self.x = []
    self.y = []

    # 토큰화한 학습데이터를 입력데이터와 출력데이터로 만드는 작업입니다.
    # window_size에 따라 각 중심 단어 X에 대해 좌, 우의 N개의 단어가 각각 Y가 됩니다.
    # 따라서 iteration 별로 (X,Y)가 총 2*window_size 만큼 나오게 됩니다.
    for tokens in tqdm(train_tokenized):
      token_ids = [w2i[token] for token in tokens]
      for i, id in enumerate(token_ids):
        if i-window_size >= 0 and i+window_size < len(token_ids):
          self.y += (token_ids[i-window_size:i] + token_ids[i+1:i+window_size+1])
          self.x += [id] * 2 * window_size

    self.x = torch.LongTensor(self.x)  # (전체 데이터 개수)
    self.y = torch.LongTensor(self.y)  # (전체 데이터 개수)

  def __len__(self):
    return self.x.shape[0]

  def __getitem__(self, idx):
    return self.x[idx], self.y[idx]

 

 

4. Modeling

다음으로 각 학습에 맞는 모델 아키텍처를 구현하겠습니다. Word2Vec 알고리즘은 기본적으로 V차원의 단어를 N차원으로 임베딩 한 후 다시 V차원으로 출력합니다. 아래 그림은 Word2Vec의 기본적인 신경망입니다. 따라서 Cbow와 SkipGram은 동일한 모델 아키텍처를 따릅니다.

class CBOW(nn.Module):
  def __init__(self, vocab_size, dim):
    super(CBOW, self).__init__()
    self.embedding = nn.Embedding(vocab_size, dim, sparse=True)
    self.linear = nn.Linear(dim, vocab_size)

 
  def forward(self, x):  
    embeddings = self.embedding(x)  
    embeddings = torch.sum(embeddings, dim=1)  
    output = self.linear(embeddings) 
    return output
class SkipGram(nn.Module):
  def __init__(self, vocab_size, dim):
    super(SkipGram, self).__init__()
    self.embedding = nn.Embedding(vocab_size, dim, sparse=True)
    self.linear = nn.Linear(dim, vocab_size)

  
  def forward(self, x):
    embeddings = self.embedding(x) 
    output = self.linear(embeddings)  
    return output

 

 

5. Train

모델 학습을 위해 Trainer class를 구현니다. 학습 방법은 간단합니다. 입력데이터를 모델을 통해 출력하고 출력한 데이터와 실제 데이터의 손실을 계산하여 파라미터를 최적화합니다.

class Trainer():
    def __init__(self,dataloader,model, optimizer, criterion, device  ):
        self.dataloader = dataloader
        self.model = model
        self.optimizer = optimizer
        self.criterion = criterion
        self.device = device
    
    def train(self,epoch=1):
        self.model.train()
        self.model.to(self.device)
        for e in range(epoch):
            print('#' * 50)
            print(f'Epoch: {e}')
            for batch in tqdm(self.dataloader):
                x, y = batch
                x, y = x.to(device), y.to(device)
                output = self.model(x)

                self.optimizer.zero_grad()
                loss = self.criterion(output,y)
                loss.backward()
                self.optimizer.step()
                print(f'Train loss : {loss.item()}')
        print('Finished.')

 

학습 진행을 위한 하이퍼 파라메터를 입력하여 학습을 진행합니다.

lr = 5e-4
num_epochs = 10
device = torch.device('cuda')

cbow_set = CBOWDataset(train_tokenized)
skipgram_set = SkipGramDataset(train_tokenized)

batch_size = 4
cbow_loader = DataLoader(cbow_set, batch_size = batch_size)
skipgram_loader = DataLoader(skipgram_set, batch_size = batch_size)

cbow = CBOW(vocab_size = len(w2i),dim = 256)
skipgram = SkipGram(vocab_size = len(w2i),dim = 256)

# Optimizer는 SGD를 사용했습니다.
cbow_optimizer = torch.optim.SGD(cbow.parameters(),lr = lr)
skipgram_optimizer = torch.optim.SGD(skipgram.parameters(), lr = lr)
criterion = nn.CrossEntropyLoss()
# Cbow와 SkipGram 모델 모두 학습하겠습니다.

cbow_trainer = Trainer(cbow_loader,cbow, cbow_optimizer, criterion, device)
cbow_trainer.train(num_epochs)
skipgram_trainer = Trainer(skipgram_loader,skipgram, skipgram_optimizer, criterion, device)
skipgram_trainer.train(num_epochs)

 

 

6. Test

학습한 모델을 활용하여 테스트할 단어와 가장 의미가 비슷한 5개 단어를 출력하는 함수를 구현합니다. 이후 원하는 단어를 입력하여 확인해보시길 바랍니다.

# 테스트할 단어와 가장 의미가 비슷하다고 판단되는 단어 5개를 출력하는 함수입니다.
# 모델은 SkipGram을 사용했습니다.
def most_similar(word,top_k = 5):
    input_id = torch.LongTensor([w2i[word]]).to(device)
    input_emb = skipgram.embedding(input_id)
    score = torch.matmul(input_emb,skipgram.embedding.weight.transpose(1,0)).view(-1)

    _,top_k_ids = torch.topk(score,top_k)

    return [i2w[word_id.item()] for word_id in top_k_ids][1:]