본문 바로가기

자연어처리

Pre-trained Word Embedding

■ 파이토치에서 임베딩 벡터를 사용하는 방법은 크게 두 가지이다. 

- 하나는 nn.Embedding( ) 모듈로 임베딩 층(embedding layer)를 만들어 훈련 데이터로부터 임베딩 벡터를 학습하는 방법

- 다른 하나는 사전에 훈련된 임베딩 벡터(pre-trained word embedding)을 가져와서 사용하는 방법이다.

1. nn.Embedding( )

■ nn.Embedding은 임베딩 층을 만들어 훈련 데이터를 임베딩 벡터로 학습하기 위해 사용한다.

그 과정에서 토큰의 정수 ID를 신경망 계산에 사용되는 임베딩 벡터로 매핑한다. 그리고 옵티마이저는 모델 가중치를 업데이트할 때, 이 임베딩 벡터값도 업데이트해서 손실을 최소화하기 때문에 모델이 풀고자하는 문제에 맞는 임베딩 벡터로 업데이트된다.

- 토큰의 정수 ID를 신경망 계산에 사용되는 임베딩 벡터로 매핑하기 때문에 Word2Vec 처럼 입력 형태가 원-핫 벡터일 필요가 없다.

■ 각 단어들을 임베딩 층의 입력으로 넣기 위해서는 각 단어들이 모두 정수로 인코딩된 상태여야 한다. 각 단어(또는 토큰)에 부여된 고유한 정수값이 임베딩 층을 통과하면 밀집 벡터가 만들어지며, 이 밀집 벡터가 임베딩 벡터이다. 

- 즉, 임베딩 벡터가 만들어지는 단계는 단어(또는 토큰) \( \rightarrow \) 단어(또는 토큰)에 고유한 정수값 부여 \( \rightarrow \) 그다음 임베딩 층에 입력 \( \rightarrow \) 임베딩 벡터로 변환

■ 특정 단어를 나타내는 고유한 정수값이 임베딩 벡터로 변환되는 과정에는 룩업 테이블 연산이 있다. 

■ 특정 단어와 매핑되는 정수값을 가지는 테이블로부터 해당 단어의 임베딩 벡터를 그대로 가져오는 작업은 룩업 테이블이라고 볼 수 있다. 

- 여기서 임베딩 벡터값을 담고 있는 테이블의 행의 개수는 고유한 단어(토큰)들을 담고 있는 단어 집합(vocabulary)의 크기이다.

■ 단어가 임베딩 벡터로 변환되는 과정은 다음과 같다. 

파이토치의 nn.Embedding( )에서는 위와 같은 룩업 테이블 결과를 얻기 위해 Word2Vec처럼 각 단어를 원-핫 벡터로 변환할 필요 없이, 단어(또는 토큰)의 정수 ID를 입력으로 넣으면 임베딩 벡터를 얻을 수 있다.

그리고 앞서 언급한 것처럼 모델 학습 과정(순전파&역전파)에서 임베딩 벡터값이 업데이트 된다.

위의 예시와 같은 룩업 테이블 연산 과정을 구현하면 다음과 같다.

sentence = "you say goodbye and i say hello"

tokens = set(sentence.split()) # 띄어쓰기 기준으로 생성된 단어 토큰들을 모아 놓은 단어 집합 생성

## 단어 집합의 각 토큰에 고유한 정수 ID 매핑
token2idx = {token: i+2 for i, token in enumerate(tokens)}
token2idx['<unk>'] = 0 # OOV 단어에 대한 토큰
token2idx['<pad>'] = 1 # 시퀀스들의 길이를 맞추기 위한 패딩 토큰

token2idx
```#결과#```
{'and': 2,
 'i': 3,
 'say': 4,
 'goodbye': 5,
 'you': 6,
 'hello': 7,
 '<unk>': 0,
 '<pad>': 1}
````````````
def create_table(rows, cols):
    matrix = torch.zeros((rows, cols)) # 처음 두 행(인덱스 0과 1)은 unk, pad 토큰에 대한 행
    matrix[2: ] = torch.rand(rows - 2, cols) # 3번째 행부터 임베딩 벡터값
    return matrix
import torch

torch.manual_seed(0)
# 테이블의 행의 개수는 단어 집합의 크기 
embedding_table = create_table(8, 3) # 테이블의 열을 3으로 = 임베딩 벡터의 차원은 3
embedding_table
```#결과#```
tensor([[0.0000, 0.0000, 0.0000],
        [0.0000, 0.0000, 0.0000],
        [0.4963, 0.7682, 0.0885],
        [0.1320, 0.3074, 0.6341],
        [0.4901, 0.8964, 0.4556],
        [0.6323, 0.3489, 0.4017],
        [0.0223, 0.1689, 0.2939],
        [0.5185, 0.6977, 0.8000]])
````````````
sample = "you say hi i say hello".split()
sample
```#결과#```
['you', 'say', 'hi', 'i', 'say', 'hello']
````````````

idx = []

# 샘플의 단어를 토큰의 정수 ID와 매핑
for word in sample:
    try:
        idx.append(token2idx[word]) # 사전에 있는 토큰이면 해당 토큰의 정수 ID를 부여
    except KeyError:
        idx.append(token2idx['<unk>']) # 사전에 없는 토큰이면 <unk> 토큰의 정수 ID를 부여

idx= torch.LongTensor(idx) # sample 토큰의 인덱스를 정수형(long)으로 
idx.dtype
```#결과#```
torch.int64
````````````

# 룩업 테이블 연산을 통해 "you say hi i say hello"의 각 단어들의 임베딩 벡터 가져오기
embedding_vector = embedding_table[idx, : ]
embedding_vector
```#결과#```
tensor([[0.0223, 0.1689, 0.2939],
        [0.4901, 0.8964, 0.4556],
        [0.0000, 0.0000, 0.0000],
        [0.1320, 0.3074, 0.6341],
        [0.4901, 0.8964, 0.4556],
        [0.5185, 0.6977, 0.8000]])
````````````

■ 위의 과정들을 nn.Embedding( )을 이용한다면 다음과 같다.

sentence = "you say goodbye and i say hello"

tokens = set(sentence.split()) # 띄어쓰기 기준으로 생성된 단어 토큰들을 모아 놓은 단어 집합 생성

## 단어 집합의 각 토큰에 고유한 정수 ID 매핑
token2idx = {token: i+2 for i, token in enumerate(tokens)}
token2idx['<unk>'] = 0 # OOV 단어에 대한 토큰
token2idx['<pad>'] = 1 # 시퀀스들의 길이를 맞추기 위한 패딩 토큰
import torch
import torch.nn as nn

embedding_layer = nn.Embedding(num_embeddings = len(token2idx),
                               embedding_dim = 3,
                               padding_idx = 1)
                               
embedding_layer
```#결과#```
Embedding(8, 3, padding_idx=1)
````````````

- \( 8 \times 3 \)크기의 임베딩 테이블이 nn.Embedding( )을 통해 생성된 것을 볼 수 있다.

■ nn.Embedding에서 중요한 두 가지 인자(parameter)는 num_embeddings와 embedding_dim이다.

- num_embeddings은 임베딩할 단어들의 개수이므로 단어 집합의 크기(길이)를 지정하면 된다.

- embedding_dim에는 사용자가 임베딩 벡터의 차원을 지정한다. 즉, 하이퍼파라미터이다.

- padding_idx는 패딩을 위한 토큰의 인덱스를 알려줄 때 사용한다. 이 예에서는 패딩 토큰인 <pad>의 정수 ID가 1이므로 padding_idx = 1로 지정한다.

 

Embedding — PyTorch 2.6 documentation

 

Embedding — PyTorch 2.6 documentation

Shortcuts

pytorch.org

■ nn.Embedding( )에는 weight라는 변수가 있다. 이 변수를 통해 행의 개수가 단어 집합의 크기인 임베딩 테이블을 참조할 수 있다. 이 weight라는 변수는 모델 학습 과정에서 업데이트되는 텐서로 정의된 학습 가능한 매개변수이다. 

embedding_layer.weight
```#결과#```
Parameter containing:
tensor([[-1.1258, -1.1524, -0.2506],
        [ 0.0000,  0.0000,  0.0000],
        [-0.3160, -2.1152,  0.4681],
        [-0.1577,  1.4437,  0.2660],
        [ 0.1665,  0.8744, -0.1435],
        [-0.1116,  0.9318,  1.2590],
        [ 2.0050,  0.0537,  0.6181],
        [-0.4128, -0.8411, -2.3160]], requires_grad=True)
````````````

- requires_grad = True로 지정된 것을 볼 수 있다.

 

2. 사전 훈련된 워드 임베딩(Pre-trained Word Embedding)

■ 이미 존재하는 말뭉치에서 잘 학습된 사전 훈련된 단어 임베딩 벡터(Pre-trained Word Embedding Vector)를 활용하는 것이 효율적이다.

- 개인이 양질의 말뭉치 데이터를 직접 구축하고 모델을 학습시키는 것은 상당한 자금과 시간이 소요되는 작업이기 때문이다.

■ torchtext의 datasets을 통해 가공되지 않은 텍스트 데이터를 불러올 수 있다. 이렇게 불러온 raw 텍스트 데이터는 torchtext의 data를 통해 전처리부터 train, valid, test set 설정까지 모델 학습을 위한 데이터 설정을 진행할 수 있다.

import torch
from torchtext import data
from torchtext import datasets

# torchtext==0.4

■ 먼저, 전처리를 어떻게 할 것인지 필드 객체를 정의할 수 있다.

# Field를 통해 전처리를 어떻게 할 것인지 적용
text = data.Field(batch_first = True,
                  fix_length = 500,
                  tokenize = str.split,
                  pad_first = True,
                  pad_token = '<pad>',
                  unk_token = '<unk>')
                  
label = data.LabelField(dtype = torch.float) # 레이블 데이터의 type은 float으로

- batch_first는 미니 배치 차원을 맨 앞으로 하여 데이터를 불러올 것인지에 대한 여부이며 False가 기본값이다. 

- fix_length는 문장의 길이를 지정하는 옵션이다. 

- tokenize는 어떤 토큰화 함수를 사용할 것인지 설정하는 옵션으로 기본값은 띄어쓰기 기반의 파이썬의 string.split 함수이다.

- pad_first는 fix_length에 지정한 문장의 길이보다 짧은 문장의 경우 패딩(padding)을 해야 하는데, 패딩을 앞에서부터 할지, 뒤에서부터 할지에 대한 옵션이다.

- pad_token에는 패딩에 대한 특수 토큰을 지정한다.

- unk_token에는 단어(또는 토큰) 사전에 없는 토큰이 나왔을 경우 해당 토큰을 표현하는 특수 토큰을 지정한다.

■ 정의한 필드 객체는 다음과 같이 데이터를 불러올 때, 레이블 데이터는 label_field에 일반 텍스트 데이터는 text_field에 지정해주면 된다.

train_data, test_data = datasets.IMDB.splits(text_field = text, label_field = label)

- 이 예시에서는 IMDB 영화 리뷰 데이터를 불러와서 사용한다.

■ torchtext.datasets에서 불러온 데이터는 examples를 통해 데이터의 개수를 확인할 수 있으며, fields를 통해 필드 객체가 어떻게 적용되었는지도 확인할 수 있다.

print('train data length', len(train_data.examples))
print('test data length', len(test_data.examples))
```#결과#```
train data length 25000
test data length 25000
````````````

print(train_data.fields)
```#결과#```
{'text': <torchtext.data.field.Field object at 0x791b48da36d0>, 
'label': <torchtext.data.field.LabelField object at 0x791b48d1c7d0>}
````````````

■ vars( ) 함수를 통해 불러온 텍스트 데이터의 값을 확인할 수 있다.

print(' '.join(vars(train_data.examples[1])['text']))
```#결과#```
A very promising directorial debut for Bill Paxton. 
A very dark thriller/who-really-done-it recommended by Stephen King. 
This is a strong, well-conceived horror tale about a devout, but demented man in Thurman, Texas that goes on a murdering spree after getting orders from God to eliminate demons trying to control mankind. 
A couple of plot twists and an eerie finale makes for your moneys worth. 
Most of the violence you don't really see, but still enough to double up your stomach.<br /><br />Director Paxton plays the twisted man to be known as the Hand of God Killer. 
Matthew McConaughey is equally impressive as the demented man's eldest son that ends up telling this story to a Dallas FBI Agent(Powers Boothe). 
Boothe, as always, is solid and flawless. Suspenseful white knuckler! Highly recommended.
````````````

## 레이블 데이터이 있는 pos와 neg는 긍정적인 리뷰, 부정적인 리뷰
print(' '.join(vars(train_data.examples[1])['label']))
```#결과#```
p o s
````````````

■ 텍스트 데이터에 대한 데이터 정제(data cleansingx)는 Field의 preprocessing 옵션을 이용해 미리 처리하거나 다음과 같이 별도로 처리할 수 있다.

import re

def preprocessing_text(sentence):
  sentence = sentence.lower() # 소문자화
  sentence = re.sub('<[^?]*>', repl=' ', string=sentence) # <br /> 처리
  sentence = re.sub('[!"#$%&\()*+,/:;<=>?@[\\]^_{|}~]', repl=' ', string=sentence) # 특수문자 처리
  sentence = re.sub('\s+', repl=' ', string=sentence) # 연속된 띄어쓰기 처리
  return sentence
for example in train_data.examples:
  vars(example)['text'] = preprocessing_text(' '.join(vars(example)['text'])).split()

for example in test_data.examples:
  vars(example)['text'] = preprocessing_text(' '.join(vars(example)['text'])).split()

■ 전처리가 끝났으면, 다음 단계는 단어 집합을 생성하는 단계이다. 다음 코드는 주어진 데이터를 build_vocab( )을 사용하여 단어 집합(사전)을 만드는 코드이다.

text.build_vocab(train_data, min_freq=2, max_size=None, vectors="glove.6B.300d")
label.build_vocab(train_data)

■ Field로 작업한 text에 build_vocab을 이용해서 텍스트 데이터와 레이블 데이터의 단어 사전을 만들 수 있다.

- min_freq는 단어 집합(사전)에 있는 단어(토큰)의 최소 등장 횟수에 제한을 둘 수 있다. 
- max_size는 단어 집합의 최대 크기를 제한을 둘 수 있다. 즉, 단어 집합의 최대 크기를 지정한다. 
- vectors는 사용할 사전 학습된 임베딩 벡터 룩업 테이블을 string 형태로 지정하는 옵션. 지정한 임베딩 벡터 룩업 테이블에 기반하여 단어 사전에 적용한다.
- glove 외에 fasttext도 사용할 수 있다. 사용할 수 있는 임베딩의 종류는 다음과 같다.

charngram.100d, fasttext.en.300d, fasttext.simple.300d, 
glove.42B.300d, glove.840B.300d, 
glove.twitter.27B.25d, glove.twitter.27B.50d, glove.twitter.27B.100d, glove.twitter.27B.200d,
glove.6B.50d, glove.6B.100d, glove.6B.200d, glove.6B.300d

■ 생성한 단어 집합 내의 단어(토큰)는 .stoi를 통해서 확인할 수 있다.

text.vocab.stoi
```#결과#```
defaultdict(<bound method Vocab._default_unk_index of <torchtext.vocab.Vocab object at 0x791b467a5c10>>,
            {'<unk>': 0,
             '<pad>': 1,
             'the': 2,
             'a': 3,
             'and': 4,
             ...,
             ...,
              'dr.': 995,
             'whom': 996,
             'bill': 997,
             'brings': 998,
             'manages': 999,
             ...})            
````````````

for token, token2idx in list(text.vocab.stoi.items())[:10]:
  print(token, token2idx)
```#결과#```
<unk> 0
<pad> 1
the 2
a 3
and 4
of 5
to 6
is 7
in 8
i 9
````````````

label.vocab.stoi.items()
```#결과#```
dict_items([('neg', 0), ('pos', 1)])
````````````

■ 이제, 다음과 같이 훈련(train), 검증(validation), 테스트(test) 세트를 구분하고 이터레이터(iterator)를 활용하여 배치 데이터를 생성하면 모델 학습을 위한 데이터 설정 단계는 

import random

train_data, valid_data = train_data.split(random_state = random.seed(0), split_ratio = 0.8) 

device = torch.device('cdua' if torch.cuda.is_available() else 'cpu')
train_loader, valid_loader, test_loader = data.BucketIterator.splits(
    datasets=(train_data, valid_data, test_data), batch_size = 32, device = device
    )
    
len(train_loader)
```#결과#```
625
````````````

type(train_loader)
```#결과#```
torchtext.data.iterator.BucketIterator
````````````

 

- 25000개 중에서 0.8은 train으로 나누었으니 train의 개수는 20000개

- 그리고 배치 크기 32씩 묶어주었으므로 훈련 데이터의 미니 배치 수는 20000/32 = 625개

 

■ iterator로 정의하였으니 next나 for 문을 이용해 다음과 같이 배치를 하나씩 꺼낼 수 있다.

for i in train_iterator:
  print(i)
```#결과#```
[torchtext.data.batch.Batch of size 32]
	[.text]:[torch.LongTensor of size 32x500]
	[.label]:[torch.FloatTensor of size 32]

[torchtext.data.batch.Batch of size 32]
	[.text]:[torch.LongTensor of size 32x500]
	[.label]:[torch.FloatTensor of size 32]
    
    ...,
    ...,
    ....
````````````
# data의 Iterator 함수로 정의할 수도 있다.

batch_size = 32
train_loader = data.Iterator(dataset = train_data, batch_size = batch_size)

len(train_loader)
```#결과#```
313
````````````
data = next(iter(train_loader))
print(data.text.shape,'\n',data.text)
```#결과#```
torch.Size([32, 500]) 
 tensor([[    1,     1,     1,  ...,    88,   137,  1280],
        [    1,     1,     1,  ...,   241,     5,   640],
        [    1,     1,     1,  ...,    51, 29944,   136],
        ...,
        [    1,     1,     1,  ...,    39, 19991,   526],
        [    1,     1,     1,  ...,     3,     0,     0],
        [    1,     1,     1,  ...,  1674,  8638,     7]])
````````````

■ 배치 데이터의 크기는 32 x 500이다. 여기서 32는 배치 크기이고 500은 각 샘플의 길이이다. 500은 fix_length에서 지정한 값이다. 

■ 즉, 미니 배치의 크기는 (batch size x fix_length)이다.

■ 그리고 샘플 길이가 500보다 작은 샘플들은 앞에 <pad> 토큰의 번호인 숫자 1로 패딩된 것을 볼 수 있다. 또한, 단어 집합에 포함되지 못한 단어는 <unk> 토큰의 정수 ID인 0으로 변환된 것을 볼 수 있다.

 

■ text.vocab.vectors를 다음과 같이 임베딩 레이어의 초깃값으로 지정할 수도 있다.

import torch.nn as nn

embedding_layer = nn.Embedding.from_pretrained(text.vocab.vectors, freeze = False)

■ 이렇게 사전 학습된 임베딩 벡터를 불러와 사용하는 것은 편리하지만, 해결하고자 하는 문제에 적합한 말뭉치 데이터를 선택하는 것이 중요하다. 예시로 사용한 IMDB는 영화 리뷰 데이터이므로 영화 리뷰에 대한 감정 분석과 같은 영화 도메인 작업에 사용하는 것이 적합하다.

■ 이러한 영화 리뷰 데이터로 생성한 워드 임베딩 벡터를 과학이나 요리 분야에 적용한다면 효과적이지 않을 것이다. 즉, 각 도메인에 맞는 말뭉치 데이터를 활용하는 것이 효과적이다. 

■ 단, 훈련 데이터의 양이 충분하지 않은 경우 해당 문제의 도메인에 특화되지 않았더라도, Word2Vec이나 GloVe 같은 사전 학습된 임베딩 벡터를 활용하면 모델 성능이 개선될 수 있다.

- gensim을 이용해 구글에서 사전 학습시킨 Word2Vec 모델을 사용할 수 있다.

import gensim.downloader as api
word2vec_model = api.load("word2vec-google-news-300")

word2vec_model.vectors.shape
```#결과#```
(3000000, 300)
````````````

word2vec_model['cat']
```#결과#```
array([ 0.0123291 ,  0.20410156, -0.28515625,  0.21679688,  0.11816406,
        0.08300781,  0.04980469, -0.00952148,  0.22070312, -0.12597656,
        ...,
        ...,
       -0.20605469,  0.18066406, -0.15820312,  0.05932617,  0.28710938,
       -0.04663086,  0.15136719,  0.4921875 , -0.27539062,  0.05615234],
      dtype=float32)        
```````````

- 모델의 크기는 3,000,000 x 300으로 3백만 개의 단어가 있으며, 각 단어의 차원은 300이다. 

- 모델에 특정 단어를 입력해서 해당 단어의 임베딩 벡터를 반환할 수 있다.

참고) [NLP] 파이토치를 이용한 임베딩 - Jay’s Blog

'자연어처리' 카테고리의 다른 글

Subword Tokenizer - (1) Byte Pair Encoding(BPE) Tokenization  (0) 2025.03.15
엘모(Embeddings from Language Model, ELMo)  (0) 2025.03.08
원-핫 인코딩, 워드 임베딩  (0) 2025.02.28
유사도  (0) 2025.02.28
Bag of Words, DTM, TF-IDF  (0) 2025.02.27