Word2Vec의 sliding window 완벽하게 이해하기

중심단어 기준으로 몇개의 단어를 볼지 그 범위를 window라고 한다.

 

왜 헷갈리기 시작했냐면 중심단어가 무조건 하나만 있다고 생각이 고정되어버리는 거임

 

예측하고자 하는 중심단어는 선택할 수가 있다.

 

무슨말이냐면 "The fat cat sat on the mat" 이 문장이 입력으로 주어졌다고 생각해보자.

 

근데 이제 그냥 중심단어를 무조건 sat이라 하고 window size=3이라 해서

 

나머지  {"The", "fat", "cat", "on", "the", "mat"}가 주변단어라고 해버리니까 생각이 멈춰버리는거임..

 

모델이 embedding vector를 구하는게 목적이라고 생각한다면

 

모든 단어에 대해서 embedding vector를 구해야할거 아니냐

그러니까 모든 단어가 중심단어가 될 수 있다는 것임.

 

이것이 바로 sliding window기법이다.

 

output으로 원하는 중심단어를 선택하고 지정한 window만큼 이동하여

model 학습을 위한 데이터셋을 만드는 과정이다.

 

1. CBOW

 

CBOW를 예로 들어 그림을 보자.

 

etc-image-0
window size=2인 경우의 그림

 

window size가 n이면 중심단어와 주변단어는 어떤 관계를 가질까? 주변단어는 2n개가 된다.

 

2*window size만큼 된다는 것이다.

 

코드 구현에서  왜 2*window size에 집착했는지 이제는 알 수 있을 것이다.

 

위 그림의 표가 바로 CBOW의 훈련데이터 세트가 된다.

 

 

2. CBOW의 데이터셋 구현

 

from tqdm import tqdm
import torch

class CBOWDataset(Dataset):
    
    def __init__(self, train_tokenized, window_size=2):
        
        self.x = []
        self.y = []
        
        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]

 

왜 입력 x가 2*window size인거 알겠지? 주변단어가 그만큼 생성되니까 그렇다.

 

from torch import nn

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)
        
    #B: batch size, W: window size, d_w: word embedding size, V: vocab_Size
    
    def forward(self,x):   #x:(B,2W)
        
        embeddings = self.embedding(x)  # (B,2W,d_w)
        
        embeddings = torch.sum(embeddings,dim=1) #(B,d_w)
        
        output = self.linear(embeddings) #(B,V)
        
        return output

클래스의 입력 사이즈에도 왜 2w인지 알겠지.. 입력이 2w만큼 들어가거든

 

참고로 embedding의 sum을 torch.sum()으로 했다는 점 주목할 만하다.

 

원래 논문의 방식을 따랐다고 한다.

 

 

 

3. Skip-gram

 

비슷하게 skip gram의 경우 훈련데이터 세트는 다음과 같이 구성된다.

 

etc-image-1

 

 

혹은 하나 하나 차분하게 살펴보고 싶으면

 

etc-image-2

 

4개 sample 추가

 

니가 여기서 착각한점은 not이 하나만 있고 나머지 주변단어 4개에 대응된다는 점인데

 

그러니까 데이터세트에 2*window size가 도저히 이해가 안되는거임

 

중심단어 하나 주변단어 하나 이렇게 넣어야 되니까 중심단어가 2*windowsize만큼 늘어남

 

아무튼 window를 이동해서

 

etc-image-3

 

마지막으로

 

etc-image-4

 

이런 skip gram의 훈련데이터셋을 얻는다

 

그럼 이제 skip gram의 데이터셋 코드도 이해할 수 있겠지?

 

from torch.utils.data import Dataset
from tqdm import tqdm

class SkipGramDataset(Dataset):
    
    def __init__(self, train_tokenized, window_size = 2):
        
        self.x = []
        self.y = []
        
        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]

 

왜 x에 2*windowsize가 붙었을까? 주변단어수가 2*windowsize만큼 있고 각각에 중심단어를 대응시키니까 그렇다

 

그러면 클래스코드를 보면

 

from torch import nn
import torch

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)
        
    #B: batch size, W: window size, d_w: word embedding size, V: vocab size
    
    def forward(self, x): # x:(B)
        
        embeddings = self.embedding(x) #(B, d_w)
        
        output = self.linear(embeddings) #(B,V)
        
        return output

 

입력으로 한 단어가 여러개 배치만큼 들어가잖아

 

etc-image-5

 

 

CBOW는 여러 단어가 들어가니까 2W로 추가 랭크가 있었고

 

결과는 이제 주변단어들 수만큼이니까 VOCAB SIZE로 나오는거임

728x90