Word2Vec Practice
- 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:]