PyTorch’s Embedding()

단어 임베딩(Word Embedding)이란 말뭉치의 각 단어에 일대일로 대응하는 실수 벡터의 집합이나 혹은 이런 집합을 구하는 행위를 Word Embedding이라고 합니다. Word2Vec도 이런 워드 임베딩의 한 방법입니다.

그렇다면 왜 이런 워드 임베딩 방법이 필요할까요? 그 이유는 컴퓨터가 자연어를 이해하지 못하기 때문입니다. 그렇다면 컴퓨터가 이해할 수 있는 형태, 즉 숫자로 단어를 바꿔서 입력해줘야 합니다. 그렇다면 어떻게 해야 효과적으로 단어를 숫자의 형태로 바꿀 수 있을까요? 이러한 고민에서 나온 것이 워드 임베딩입니다.

먼저 워드를 숫자로 바꾸는 가장 간단한 방법은 원-핫-인코딩(One-Hot-Encoding)입니다. 예를 들어 “나는 학교에 갑니다” 이 문장을 3개의 단어로 구분하고 각 단어의 위치를 표시하는 것이죠. 이렇게 하면 일단 문자를 숫자로 바꾸는데는 성공했습니다.

1나는1,0,0
2학교에0,1,0
3갑니다0,0,1

그러나 이러한 방법에는 단점이 있습니다. 가장 큰 단점은 벡터의 사이즈가 너무 커진다는 것과 벡터의 내용이 하나의 1을 제외한 나머지 내용이 모두 0으로 채워진다는 것입니다. 예를 들어 “나는 학교에 갑니다” 3개의 단어이지만 책과 같은 대규모의 말뭉치에 등장하는 단어는 수만개가 된다는 것이죠. 그렇게 되면 벡터의 크기는 수만개가 넘는 사이즈에 대부분 0인 벡터가 만들어지기 때문에 이를 처리하는데 큰 문제가 생깁니다. 이것은 희소 벡터(Sparse Vector)라고 합니다. 또 하나의 문제는 각 단어의 값들은 모두 동일한 거리(Distance)를 가진다는 것입니다. 의미론적인 구분이 불가능하다는 것이죠.

그렇기 때문에 필요한 것은 단어의 크기와 상관 없는 차원의 벡터와 0과 1이 아닌 실수값을 가지는 새로운 벡터가 필요합니다. 이것을 밀집 벡터(Dense Vector)라고 합니다. 또 각 단어가 가지는 벡터에 방향성이나 유사도에 따라서 거리가 가깝거나 멀거나 하는 특징을 가지도록 표현할 필요가 있습니다.

워드 임베딩은 이러한 밀집된 형태의 벡터를 만드는 과정이라고 할 수 있습니다.

PyTorch는 입력 텍스트를 받아서 임베딩 벡터를 생성하는 nn.Eembedding()을 제공하고 있습니다. index 값이 부여되어 있는 단어를 입력 받습니다. 여기서 index는 고유한 값이 됩니다. 이 Index를 참조 테이블(look-up table)에서 사용할 것입니다. 즉, |?|×? 크기의 행렬에 단어 임베딩을 저장하는데, D 차원의 임베딩 벡터가 행렬의 ?i 번째 행에 저장되어있어 ?i 를 인덱스로 활용해 임베딩 벡터를 참조하는 것입니다. 여기서 |?|는 vocabulary의 수이고 D는 차원정보입니다.

https://wikidocs.net/64779

위의 그림은 단어 great이 정수 인코딩 된 후 테이블로부터 해당 인덱스에 위치한 임베딩 벡터를 꺼내오는 모습을 보여줍니다. 위의 그림에서는 임베딩 벡터의 차원이 4로 설정되어져 있습니다. 그리고 단어 great은 정수 인코딩 과정에서 1,918의 정수로 인코딩이 되었고 그에 따라 단어 집합의 크기만큼의 행을 가지는 테이블에서 인덱스 1,918번에 위치한 행을 단어 great의 임베딩 벡터로 사용합니다. 이 임베딩 벡터는 모델의 입력이 되고, 역전파 과정에서 단어 great의 임베딩 벡터값이 학습됩니다.

룩업 테이블의 개념을 이론적으로 우선 접하고, 처음 파이토치를 배울 때 어떤 분들은 임베딩 층의 입력이 원-핫 벡터가 아니어도 동작한다는 점에 헷갈려 합니다. 파이토치는 단어를 정수 인덱스로 바꾸고 원-핫 벡터로 한번 더 바꾸고나서 임베딩 층의 입력으로 사용하는 것이 아니라, 단어를 정수 인덱스로만 바꾼채로 임베딩 층의 입력으로 사용해도 룩업 테이블 된 결과인 임베딩 벡터를 리턴합니다.[1]

참고 : Word Embeddings in Pytorch

https://pytorch.org/tutorials/beginner/nlp/word_embeddings_tutorial.html

파이토치 공식홈에 있는 내용을 사용해서 간단한 테스트 코드를 만들어보겠습니다.

import torch
import torch.nn as nn

train_data = '태초에 하나님이 천지를 창조하시니라 창세기 1장 1절'.split(' ')
word_set = list(set(train_data))
word_to_ix = {tkn:i for i, tkn in enumerate(word_set)}
word_to_ix

입력 데이터를 글자 단위로 분리하여 dict에 저장합니다. 저장된 내용을 출력해보면 아래의 내용과 같습니다. 중복을 제거하기 위해 set() 자료형을 사용해서 단어의 순서는 무시되었습니다.

{'천지를': 0, '하나님이': 1, '창조하시니라': 2, '창세기': 3, '태초에': 4, '1절': 5, '1장': 6}

이제 이렇게 분리한 단어를 아래와 같은 방법으로 one-hot 형태로 나타내면 다음과 같은 형태로 표시됩니다.

one_hot = []
for i, tkn in enumerate(word_to_ix):
    one_hot.append(np.eye(len(vocab), dtype='int')[word_to_ix[tkn]])
[array([1, 0, 0, 0, 0, 0, 0]), #천지를
 array([0, 1, 0, 0, 0, 0, 0]), #하나님이
 array([0, 0, 1, 0, 0, 0, 0]), #창조하시니라
 array([0, 0, 0, 1, 0, 0, 0]), #창세기
 array([0, 0, 0, 0, 1, 0, 0]), #태초에
 array([0, 0, 0, 0, 0, 1, 0]), #1절
 array([0, 0, 0, 0, 0, 0, 1])] #1장

그러나 서두에 언급했듯이 이렇게 표현된 벡터 데이터를 직접 학습에 사용하기는 적절하지 않습니다. 그래서 차원을 입력 데이터의 차원을 낮춰주고 Sparse한 데이터를 Dense한 형태로 변경할 필요가 있습니다. 그때 사용하는 것이 Embedding이라고 할 수 있습니다.

torch.nn.Embedding()은 이러한 작업을 쉽게 할 수 있도록 도와줍니다.

embeds = nn.Embedding(len(vocab), 3)
lookup_tensor = torch.tensor([word_to_ix["태초에"]], dtype=torch.long)
w = embeds(lookup_tensor)
print(w)

위와 같은 방법을 사용하면 [1, 0, 0, 0, 0, 0, 0] 형태의 one-hot 벡터를 [-1.0998, -1.0605, -0.5849] 형태의 데이터로 변경할 수 있습니다.

#생성된 weight 벡터
Parameter containing:
tensor([[ 0.5765,  0.2391, -0.1834],
        [-0.1860, -0.0754,  0.4587],
        [-0.9538, -0.6950,  0.5682],
        [-2.1076, -0.4070,  0.2598],
        [-1.0998, -1.0605, -0.5849],
        [ 1.1632, -0.8139,  0.1154],
        [ 0.9705,  0.3963,  0.8804]], requires_grad=True)

아래의 링크는 Word2Vec을 실제로 어떻게 사용하는지에 대한 예제입니다. 궁금하신 분들은 참고하시기 바랍니다.

Reference

[1] https://wikidocs.net/64779

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 항목은 *(으)로 표시합니다