[파이토치 트랜스포머를 활용한 자연어 처리와 컴퓨터 비전 심층학습] 임베딩(2)
Word2Vec / fastText
Word2Vec
Word2Vec은 단어 간의 유사성을 측정하기 위해 분포 가설(distributional hypothesis)을 기반으로 개발된 임베딩 모델이다.
- 분포 가설: 같은 문맥에서 함께 자주 나타나는 단어들은 서로 유사한 의미를 가질 가능성이 높다는 가정이며 단어 간의 동시 발생(co-occurrence) 확률 분포를 이용해 단어 간의 유사성을 측정
ex. ‘내일 자동차를 타고 부산에 간다’ 와 ‘내일 비행기를 타고 부산에 간다’ 라는 두 문장에서 ‘자동차’와 ‘비행기’는 주변에 분포한 단어들이 동일하거나 유사하므로 두 단어는 비슷한 의미를 가질 것이라고 예상
가정을 통해 단어의 분산 표현(Distributed Representation)을 학습할 수 있다.
- 분산 표현: 단어를 고차원 벡터 공간에 매핑하여 단어의 의미를 담은 것을 의미
분포 가설에 따라 유사한 문맥에서 등장하는 단어는 비슷한 벡터 공간상 위치를 갖게 된다. 위의 예시에서 ‘비행기’와 ‘자동차’는 벡터 공간에서 서로 가까운 위치에 표현된다.
이는 빈도 기반 벡터화 기법에서 발생했던 단어의 의미 정보를 저장하지 못하는 한계를 극복했으며, 대량의 텍스트 데이터에서 단어 간의 관계를 파악하고 벡터 공간상에서 유사한 단어를 군집화해 단어의 의미 정보를 효과적으로 표현한다.
단어 벡터화
단어를 벡터화하는 방법은 크게 희소 표현(sparse representation)과 밀집 표현(dense representation)으로 나눌 수 있다.
- 희소 표현: 빈도 기반 방법으로 대표적으로는 원-핫 인코딩, TF-IDF가 존재
- 대부분의 벡터 요소가 0으로 표현
- 단어 사전의 크기가 커지면 벡터의 크기도 커지므로 공간적 낭비가 발생
- 단어 간의 유사성을 반영하지 못함
- 벡터 간의 유사성을 계산하는 데도 많은 비용이 발생
단어 소 0 1 0 0 0 잃고 1 0 0 0 0 외양간 0 0 0 1 0 고친다 0 0 0 0 1 - 밀집 표현: Word2Vec
- 단어를 고정된 크기의 실수 벡터로 표현하기 때문에 단어 사전의 크기가 커지더라도 벡터의 크기가 커지지 않음
- 벡터 공간상에서 단어 간의 거리를 효과적으로 계산할 수 있으며, 벡터의 대부분이 0이 아닌 실수로 이루어져 있어 효율적으로 공간을 활용
단어 소 0.3914 -0.1749 … 0.5912 0.1321 잃고 -0.2893 0.3814 … -0.1492 -0.2814 외양간 0.4812 0.1214 … -0.2745 0.0132 고친다 -0.1314 -0.2809 … 0.2014 0.3016
밀집 표현 벡터화는 학습을 통해 단어를 벡터화하기 때문에 단어의 의미를 비교할 수 있다. 밀집 표현된 벡터를 단어 임베딩 벡터(Word Embedding Vector)라고 하며, Word2Vec은 대표적인 단어 임베딩 기법 중 하나다.
Word2Vec은 밀집 표현을 위해 CBoW와 Skip-gram이라는 두 가지 방법을 사용한다.
CBoW
CBoW(Continuous Bag of Words)란 주변에 있는 단어를 가지고 중간에 있는 단어를 예측하는 방법이다.
- 중심 단어(Center Word): 예측해야 할 단어를 의미
- 주변 단어(Context Word): 예측에 사용되는 단어들
중심 단어를 맞추기 위해 몇 개의 주변 단어를 고려할지를 정해야 하는데, 이 범위를 윈도(Window)라고 한다. 이 윈도를 활용해 주어진 하나의 문장에서 첫 번째 단어부터 중심 단어로 하여 마지막 단어까지 학습한다.
- 윈도가 N일 때, 범위는 중심 단어의 앞에 위치한 N개의 주변 단어부터 뒤에 위치한 N개의 주변 단어이다.
학습을 위해 윈도를 이동해 가며 학습하는데, 이러한 방법을 슬라이딩 윈도(Sliding Window)라 한다. CBoW는 슬라이딩 윈도를 사용해 한 번의 학습으로 여러 갱의 중심 단어와 그에 대한 주변 단어를 학습할 수 있다.
위의 그림은 하나의 입력 문장에서 윈도 크기가 2일 때 학습 데이터가 어떻게 구성되는지를 보여준다.
학습 데이터는 (주변 단어 \ 중심 단어)로 구성된다. 이를 통해 대량의 말뭉치에서 효율적으로 단어의 분산 표현을 학습할 수 있다. 얻어진 학습 데이터는 인공 신경망을 학습하는데 사용된다.
CBoW 모델은 각 입력 단어의 원-핫 벡터를 입력값으로 받는다. 입력 문장 내 모든 단어의 임베딩 벡터를 평균 내어 중심 단어의 임베딩 벡터를 예측한다.
- 입력 단어는 원-핫 벡터로 표현돼 투사층(Projection Layer)에 입력된다.
- 투사층: 원-핫 벡터의 인덱스에 해당하는 임베딩 벡터를 반환하는 순람표(Lookup table, LUT) 구조
- 투사층을 통과하면 각 단어는 E 크기의 임베딩 벡터로 변환한다.
- 입력된 임베딩 벡터 $V_1, V_2, … , V_n$들의 평균값을 계산
- 계산된 평균 벡터를 가중치 행렬 $W’_{E \times V}$와 곱하면 $V$ 크기의 벡터를 얻는다.
- 소프트맥스 함수를 이용해 중심 단어를 예측한다.
Skip-gram
Skip-gram은 CBoW와 반대로 중심 단어를 입력으로 받아서 주변 단어를 예측하는 모델이다.
- 중심 단어를 기준으로 양쪽으로 윈도 크기만큼의 단어들을 주변 단어로 삼아 훈련 데이터세트를 만든다.
- 중심 단어와 각 주변 단어를 하나의 쌍으로 하여 모델을 학습시킨다.
Skip-gram과 CBoW는 학습 데이터의 구성 방식에 차이가 있다.
- CBoW: 하나의 윈도에서 하나의 학습 데이터가 만들어짐
- Skip-gram: 중심 단어와 주변 단어를 하나의 쌍으로 하여 여러 학습 데이터가 만들어짐
데이터 구성 방식 차이 때문에 Skip-gram은 하나의 중심 단어를 통해 여러 개의 주변 단어를 예측하므로 더 많은 학습 데이터세트를 추출할 수 있으며, 일반적으로 CBoW보다 더 뛰어난 성능을 보인다.
Skip-gram은 비교적 드물게 등장하느 단어를 더 잘 학습할 수 있게 되고 단어 벡터 공간에서 더 유의미한 거리 관계를 형성할 수 있다.
- 입력 단어의 원-핫 벡터를 투사층에 입력하여 해당 단어의 임베딩 벡터를 가져온다.
- 입력 단어의 임베딩과 $W’_{E \times V}$ 가중치와의 곱셈을 통해 $V$ 크기의 벡터를 얻는다.
- $V$ 벡터에 소프트맥스 연산을 취하여 주변 단어를 예측한다.
소프트맥스 연산은 모든 단어를 대상으로 내적 연산을 수행한다. 말뭉치의 크기가 커지면 필연적으로 단어 사전의 크기도 커지므로 대량의 말뭉치를 통해 Word2Vec 모델을 학습할 때 학습 속도가 느려지는 단점이 있다.
단점을 보완하는 방법은 계층적 소프트맥스와 네거티브 샘플링 기법을 적용해 학습 속도가 느려지는 문제를 완화할 수 있다.
계층적 소프트맥스
계층적 소프트맥스(Hierachical Softmax)는 출력층을 이진 트리(Binary tree) 구조로 표현해 연산을 수행한다.
- 자주 등장하는 단어일수록 트리의 상위 노드에 위치
- 드물게 등장하는 단어일수록 하위 노드에 배치
각 노드는 학습이 가능한 벡터를 가지며, 입력값은 해당 노드의 벡터와 내적값을 계산한 후 시그모이드 함수를 통해 확률을 계산한다.
잎 노드(Leaf Node)는 가장 깊은 노드로, 각 단어를 의미하며, 모델은 각 노드의 벡터를 촤적화하여 단어를 잘 예측할 수 있게 한다. 각 단어의 확률은 경로 노드의 확률을 곱해서 구할 수 있다.
ex. ‘추천해요’ → $0.43 \times 0.74 \times 0.27 = 0.085914$ 의 확률을 갖게 된다. 이 경우 학습 시 1, 2번 노드의 벡터만 최적화하면 된다.
단어 사전 크기를 $V$라고 했을 때 일반적은 소프트맥스 연산은 $O(V)$의 시간 복잡도를 갖지만, 계층적 소프트맥스의 시간 복잡도는 $O(log_2 \ V)$의 시간 복잡도를 갖는다.
네거티브 샘플링
네거티브 샘플링(Negative Sampling)은 Word2Vec 모델에서 사용되는 확률적인 샘플링 기법으로 전체 단어 집합에서 일부 단어 샘플링하여 오답 단어로 사용한다.
학습 윈도 내에 등장하지 않는 단어를 n개 추출하여 정답 단어와 함께 소프트맥스 연산을 수행한다. 이를 통해 전체 단어의 확률을 계산할 필요 없이 모델을 효율적으로 학습할 수 있다.
- n은 일반적으로 5 ~ 20개를 사용
네거티브 샘플링의 추출 확률은 아래 수식을 통해 구할 수 있다.
\[P(w_i) = \frac {f(w_i)^{0.75}}{\sum_{j = 0}^{V}f(w_j)^{0.75}}\]- $f(w_i)$: 각 단어 $w_i$의 출형 빈도수
- ‘추천해요’ 100번 등장하고 전체 단어의 빈도가 2000이라면 $f(추천해요) = \frac{100}{2000} = 0.05$
- $P(w_i)$: 단어 $w_i$가 네거티브 샘플로 추출될 확률
- 출현 빈도수에 0.75제곱한 값을 정규화 상수로 사용하는데, 이 값은 실험을 통해 얻어진 최적의 값이다.
네거티브 샘플링에서는 입력 단어 쌍이 데이터로부터 추출된 단어 쌍인지, 아니면 네거티브 샘플링으로 생성된 단어 쌍인지 이진 분류를 한다. 이를 위해 로지스틱 회귀 모델을 사용하며, 이 모델의 학습 과정에서는 추출할 단어의 확률 분포를 구하기 위해 먼저 각 단어에 대한 가중치를 학습한다.
네거티브 샘플링 Word2Vec 모델은 실제 데이터에서 추출된 단어 쌍은 1로, 네거티브 샘플링을 통해 추출된 가짜 단어쌍은 0으로 레이블링한다. 즉, 다중 분류에서 이진 분류로 학습 목적이 바뀌게 된다.
네거티브 샘플링 모델에서는 입력 단어의 임베딩과 해당 단어가 맞는지 여부를 나타내는 레이블(1 또는 0)을 가져와 내적 연산을 수행한다. 내적 연산을 통해 얻은 값은 시그모이드 함수를 통해 확률값으로 변환된다.
- 레이블이 1인 경우: 해당 확률값이 높아지도록 가중치를 최적화
- 레이블이 0인 경우: 해당 확률값이 낮아지도록 가중치를 최적화
모델 실습: Skip-gram
Word2Vec 모델은 학습할 단어의 수를 $V$로, 임베딩 차원을 $E$로 설정해 $W_{V \times E}$ 행렬과 $W’_{E \times V}$ 행렬을 최적화하며 학습한다.
- $W_{V \times E}$ 행렬은 룩업(Lookup) 연산을 수행하며 이는 임베딩(
Embedding) 클래스를 사용하여 구현이 가능
임베딩 클래스는 단어나 범주형 변수와 같은 이산 변수를 연속적인 벡터 형태로 변환해 사용할 수 있다. 연속적인 벡터 표현은 모델이 학습하는 동안 단어의 의미와 관련된 정보를 포착하고, 이를 기반으로 단어 간의 유사도를 계산한다.
1
2
3
4
5
6
7
8
# 임베딩 클래스
embedding = torch.nn.Embedding(
num_embeddings,
embedding_dim,
padding_idx=None,
max_norm=None,
norm_type=2.0
)
num_embeddings: 이산 변수의 개수로 단어 사전의 크기를 의미embedding_dim: 임베딩 벡터의 차원 수로 임베딩 벡터의 크기를 의미padding_idx: 패딩 토큰의 인덱스를 지정해 해당 인덱스의 임베딩 벡터를 0으로 설정- 병렬 처리는 입력 배치의 문장 길이가 동일해야 하므로 입력 분장들을 일정한 길이로 설정
norm_type: 임베딩 벡터의 크기를 제한하는 방법을 선택- 기본값은 2로 L2 정규화 방식을 사용하며 1로 설정하면 L1 정규화 방식을 사용한다.
max_norm: 임베딩 벡터의 최대 크기를 지정- 각 임베딩 벡터의 크기가 최대 노름 값 이상이면 임베딩 벡터를 최대 노름 크기로 잘라내고 크기를 감소시킨다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 기본 Skip-gram 클래스
from torch import nn
class VanillaSkipgram(nn.Module):
def __init__(self, vocab_size, embedding_dim):
super().__init__()
self.embedding = nn.Embedding(
num_embeddings=vocab_size,
embedding_dim=embedding_dim
)
self.linear = nn.Linear(
in_features=embedding_dim,
out_features=vocab_size
)
def forward(self, input_ids):
embeddings = self.embedding(input_ids)
output = self.linear(embeddings)
return output
기본 형식의 Skip-gram 모델은 입력 단어와 주변 단어를 룩업 테이블에서 가져와서 내적을 계산한 다음, 손실 함수를 통해 예측 오차를 최소화하는 방식으로 학습된다.
1
2
3
4
5
6
7
8
9
10
# 영화 리뷰 데이터세트 전처리
import pandas as pd
from Korpora import Korpora
from konlpy.tag import Okt
corpus = Korpora.load("nsmc")
corpus = pd.DataFrame(corpus.test)
tokenizer = Okt()
tokens = [tokenizer.morphs(review) for review in corpus.text]
데이터세트를 Okt 토크나이저를 사용해 형태소를 추출하고 이를 통해 단어 사전을 구축한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 단어 사전 구축
from collections import Counter
def build_vocab(corpus, n_vocab, special_tokens):
counter = Counter()
for tokens in corpus:
counter.update(tokens)
vocab = special_tokens
for token, count in counter.most_common(n_vocab):
vocab.append(token)
return vocab
vocab = build_vocab(corpus=tokens, n_vocab=5000, special_tokens=["<unk"])
token_to_id = {token: idx for idx, token in enumerate(vocab)}
id_to_token = {idx: token for idx, token in enumerate(vocab)}
Okt 토크나이저를 통해 토큰화된 데이터를 활용해 build_vocab 함수로 단어 사전을 구축한다.
n_vocab: 구축할 단어 사전의 크기- 문서 내에
n_vocab보다 많은 종류의 토큰이 있다면, 가장 많이 등장한 토큰 순서로 사전을 구축
- 문서 내에
special_tokens: 특별한 의미를 갖는 토큰들을 의미<unk>토큰은 OOV에 대응하기 위한 토큰으로 단어 사전 내에 없는 모든 단어는<unk>토큰으로 대체
단어 사전의 크기는 구축할 단어 사전의 크기와 특수 토큰(Special Token)의 크기 합과 동일하다.
그 다음은 윈도 크기를 정의하고 학습에 사용될 단어 쌍을 추출한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Skip-gram의 단어 쌍 추출
def get_word_pairs(tokens, window_size):
pairs=[]
for sentence in tokens:
sentence_length = len(setence)
for idx, center_word in enumerate(sentence):
window_start = max(0, idx - window_size + 1)
window_end = min(sentence_length, idx + window_size + 1)
center_word = sentence[idx]
context_words = sentence[window_start:idx] + sentence[idx+1:window_end]
for context_word in context_words:
pairs.append([center_word, context_word])
return pairs
word_pairs = get_word_pairs(tokens, window_size=2)
get_word_pairs 함수는 토큰을 입력받아 Skip-gram 모델의 입력 데이터로 사용할 수 있게 전처리한다.
window_size: 주변 단어를 몇 개까지 고려할 것인지를 설정한다.- 각 문장에서는 중심 단어와 주변 단어를 고려하여 쌍을 생성
idx: 현재 단어의 인덱스를 나타냄center_word: 중심 단어window_start&window_end: 현재 단어에서 얼마나 멀리 떨어진 주변 단어를 고려할 것인지를 결정- 문장의 경계를 넘어가는 경우가 없게 조정
출력 결과는 각 단어 쌍이 [중심 단어, 주변 단어]로 구성되어 있다. 임베딩 층은 단어의 인덱스를 입력으로 받기 때문에 단어 쌍을 인덱스 쌍으로 변환해야 한다.
1
2
3
4
5
6
7
8
9
10
11
def get_index_pairs(word_pairs, token_to_id):
pair = []
unk_idx = token_to_id["<unk>"]
for word_pair in word_pairs:
center_word, context_word = word_pair
center_index = token_to_id.get(center_word, unk_index)
context_index = token_to_id.get(context_word, unk_index)
pairs.append([center_index, context_index])
return pairs
index_pairs = get_index_pairs(word_pairs, token_to_id)
get_index_pairs 함수는 get_word_pairs 함수에서 생성된 단어 쌍을 토큰 인덱스 쌍으로 변환한다.
word_pairs단어와 해당 단어의 ID를 매핑한 딕셔너리인token_to_id로 인덱스 쌍을 생성get메서드로 토큰이 단어 사전 내에 있으면 해당 토큰의 인덱스를 반환하고, 단어 사전 내에 없다면<unk>토큰의 인덱스를 반환
생성된 인덱스 쌍은 Skip-gram 모델의 입력 데이터로 사용되며 이를 학습에 사용하기 위해서는 텐서 형식으로 변환해야한다.
1
2
3
4
5
6
7
8
9
10
# 데이터로더 적용
import torch
from torch.utils.data import TensorDataset, DataLoader
index_pairs = torch.tensor(index_pairs)
center_indexs = index_pairs[:, 0]
contenxt_indexs = index_pairs[:, 1]
dataset = TensorDataset(center_indexs, context_indexs)
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)
index_pairs는 get_index_pairs 함수에서 생성된 중심 단어와 주변 단어 토큰의 인덱스 쌍으로 이루어진 리스트다. 이 리스트를 텐서 형식으로 변환한다. 이 텐서는 [N, 2]의 구조를 가지므로 중심 단어와 주변 단어로 나눠 데이터세트로 변환한다.
인덱스 싸을 텐서 데이터세트로 변환하고 데이터로더에 적용했다면 모델을 학습하기 위한 준비 작업을 진행한다.
1
2
3
4
5
6
7
# Skip-gram 모델 준비 작업
from torch import optim
device = "cuda" if torch.cuda.is_available() else "cpu"
word2Vec = VaillaSkipgram(vocab_size=len(token_to_id), embedding_dim=128).to(device)
criterion = nn.CrossEntropyLoss().to(device)
optimizer = optim.SGD(word2vec.parameters(), lr=0.1)
VanillaSkipgram 클래스의 단어 사전 크기(vocab_size)에 전체 단어 집합의 크기를 전달하고 임베딩 크기(embedding_dim)는 128로 할당한다.
손실함수는 단어 사전 크기만큼 클래스가 있는 분류 문제이므로 교차 엔트로피를 사용하고 교차 엔트로피는 내부적으로 소프트맥스 연산을 수행하므로 신경망의 출력값을 후처리 없이 활용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 모델 학습
for epoch in range(10):
cost = 0.0
for input_ids, target_ids in dataloader:
input_ids = input_ids.to(device)
target_ids = target_ids.to(device)
logits = word2Vec(input_ids)
loss = criterion(logits, target_ids)
optimizer.zero_grad()
loss.backward()
optimizer.step()
cost += loss
cost = cost / len(dataloader)
print(f"Epoch: {epoch+1:4d}, Cost : {cost:.3f}")
# Epoch: 1, Cost : 6.177
# Epoch: 2, Cost : 5.992
# ...
# Epoch: 9, Cost : 5.805
# Epoch: 10, Cost : 5.791
모델 학습이 완료되면 $W_{V \times E}$ 행렬과 $W_{E \times V}’$ 행렬 중 하나의 행렬을 선택해 임베딩 값을 추출한다. 임베딩 층으로 구현된 $W_{V \times E}$ 행렬에서 임베딩 값을 추출하는 코드는 아래와 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 임베딩 값 추출
token_to_embedding = dict()
embedding_matrix = word2Vec.embedding.weight.detach().cpu().numpy()
for word, embedding in zip(vocab, embedding_matrix):
token_to_embedding[word] = embedding
index = 30
token = vocab[30]
token_embedding = token_to_embedding[token]
print(token)
print(token_embedding)
# 연기
# [-0.3942838 -0.09151211 0.53217596 -1.1725438 0.48068285 -0.65455276
# ...
# -0.8654873 -0.22460045]
임베딩 값으로 단어 간의 유사도를 확인할 수 있다. 임베딩 유사도를 측정할 때는 코사인 유사도(Cosine Similarity)가 가장 일반적으로 사용되는 방법이다.
- 코사인 유사도는 두 벡터 간의 각도를 이용하여 유사도를 계산
- 두 벡터가 유사할수록 값이 1에 가까워지고, 다를수록 0에 가까워진다.
두 벡터 간의 코사인 유사도는 두 벡터의 내적을 벡터의 크기(유클리드 노름)의 곱으로 나누어 계산할 수 있다.
\[cosine\ similarity(a, b) = \frac{a \cdot b}{||a||||b||}\]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 단어 임베딩 유사도 계산
import numpy as np
from numpy.linalg import norm
def cosine_similarity(a, b):
cosine = np.dot(b, a) / (norm(b, axis=1) * norm(a))
return cosine
def top_n_index(cosine_matrix, n):
closest_indexes = cosine_matrix.argsort()[::-1]
top_n = closest_indexes[1: n + 1]
return top_n
cosine_matrix = consine_similarity(token_embedding, embedding_matrix)
top_n = top_n_index(cosine_matrix, n=5)
print(f"{token}와 가장 유사한 5개 단어")
for index in top_n:
print(f"{id_to_token[index]} - 유사도 : {cosine_matrix[index]:.4f}")
# 연기와 유사한 5개 단어
# 연기력 - 유사도: 0.3976
# 배우 - 유사도: 0.3167
# 시나리오 - 유사도: 0.3130
# 악마 - 유사도: 0.2977
# 까지도 - 유사도: 0.2892
cosine_similarity함수는 입력 단어와 단어 사전 내의 모든 단어와의 코사인 유사도를 계산한다.a매개변수는 임베딩 토큰을 의미b매개변수는 임베딩 행렬을 의미하며 [5001, 128]의 구조를 가지므로 노름을 계산할 때axis=1방향으로 계산한다.
top_n_index함수는 유사도 행렬을 내림차순으로 정렬해 어떤 단어가 가장 가까운 단어인지 반환한다.- 입력 단어도 단어 사전에 포함되므로 입력 단어 자신이 가장 가까운 단어가 된다.
모델 실습: Gensim
Word2Vec 모델을 학습할 때 데이터 수가 적음에도 불구하고 오랜 시간이 걸린다. 이러한 경우, 계층적 소프트맥스나 네거티브 샘플링 같은 기법을 사용하면 더 효율적으로 학습할 수 있다.
젠심(Gensim) 라이브러리를 활용하면 자연어 처리 모델을 쉽게 구성할 수 있다.
- 대용량 텍스트 데이터의 처리를 위한 메모리 효율적인 방법을 제공해 대규모 데이터 세트에서도 효과적으로 모델을 학습
- 학습된 모델을 저장하여 관리할 수 있고, 비슷한 단어 찾기 등 유사도와 관련된 기능도 제공
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Word2Vec 클래스
word2Vec = gensim.models.Word2Vec(
sentences=None,
corpus_file=None,
vector_size=100,
alpha=0.025,
window=5,
min_count=5,
workers=3,
sg=0,
hs=0,
cbow_mean=1,
negative=5,
ns_exponent=0.75,
max_final_vocab=None,
epochs=5,
batch_words=10000
)
sentences: 모델의 학습 데이터를 나타내며 토큰 리스트로 표현된다.corpus_file: 학습 데이터를 파일로 입력할 때 파일 경로를 의미(입력 문장 대신 사용 가능)vector_size: 학습할 임베딩 벡터의 크기를 의미하며, 임베딩 차원 수를 설정alpha: Word2Vec 모델의 학습률을 의미window: 학습 데이터를 생성할 윈도의 크기min_count: 학습에 사용할 단어의 최소 빈도- 최소 빈도만큼 등장하지 않으면 학습에 사용되지 않음
max_final_vocab: 단어 사전의 최대 크기- 최소 빈도를 충족하는 단어가 최대 최종 단어 사전보다 많으면 자주 등장하는 순으로 단어 사전 구축
workers: 빠른 학습을 위해 병렬 학습할 스레드의 수sg: Skip-gram 모델의 사용 여부- 1이면 Skip-gram 모델 사용
- 0이면 CBoW 모델 사용
hs: 계층적 사용 여부를 설정(1일 떄 사용)cbow_mean: CBoW 모델로 구성할 때 합한 벡터의 평균화 여부를 설정(1일 떄 평균화)negative: 네거티브 샘플링의 단어 수(0이면 사용하지 않음)ns_exponent: 네거티브 샘플링 확률의 지수epochs: 학습 에폭 수batch_words: 몇 개의 단어로 학습 배치를 구성할지 결정
1
2
3
4
5
6
7
8
9
10
11
12
# Word2Vec 모델 학습
from gensim.models import Word2Vec
word2Vec = Word2Vec(
sentences=tokens,
vector_size=128,
window=5,
min_count=1,
sg=1,
epochs=3,
max_final_vocab=10000
)
모델 학습 속도는 VanillaSkipgram 클래스보다 훨씬 빠르게 학습된다.
1
2
3
4
5
6
7
8
9
word = "연기"
print(word2vec.wv[word])
print(word2vec.wv.most_similar(word, topn=5))
print(word2vec.wv.similar(w1=word, w2="연기력"))
# [-0.4074033 -0.19263862, ...,
# ...
# -0.36874628 -0.41801444]
# [("연기력", 0.7762452363967896), ('캐스팅', 0.7704317569732666), ('연기자', 0.7353872060775757), ('여배우', 0.7160670161247253), ('조연', 0.7131801247596741)]
word2vec 인스턴스의 wv 속성은 학습된 단어 벡터 모델을 포함한 Word2VecKeyedVectors 객체를 반환한다. 이 객체는 단어 벡터 검색과 유사도 계산 등의 작업을 수행한다.
이 객체는 유사한 단어를 찾아주는 most_similar 메서드와 두 단어 간의 유사도를 계산하는 similarity 메서드를 제공한다.
하지만 Word2Vec은 분포 가설을 통해 쉽고 빠르게 단어의 임베딩을 학습하지만, 단어의 형태학적 특징을 반영하지 못한다. 특히 한국어는 어근과 접사, 조사 등으로 이루어지는 규칙이 있기 때문에 Word2Vec 모델이 구조적 특징을 제대로 학습하기 어렵다. 이는 OOV를 발생시키는 원인이 된다.
fastText
fastText는 임베딩 모델로, 텍스트 분류 및 텍스트 마이닝을 위한 알고리즘이다.
- 단어와 문장을 벡터로 변환하는 기술을 기반으로 함
- 머신러닝 알고리즘이 텍스트 데이터를 분석하고 이해하는 데 사용
fastText에서는 단어의 벡터화를 위해 <, >와 같은 특수 기호를 사용하여 단어의 시작과 끝을 나타낸다. 이 기호가 단어의 하위 문자열을 고려하는 데 중요한 역할을 한다.
기호가 추가된 단어는 N-gram을 사용하여 하위 단어 집합(Subword set)으로 분해된다.
ex. ‘서울특별시’ → ‘서울’, ‘울특’, ‘특별’, ‘별시’
분해된 하위 단어 집합에는 나누지 않은 단어 자신도 포함되며, 단어 집합이 만들어지면 각 하위 단어는 고유한 벡터값을 갖게 되며 이는 단어의 벡터 표현을 구성하며, 이를 사용해 자연어 처리 작업을 수행한다.
- 토큰의 양 끝에 ‘<’와 ‘>’를 붙여 토큰의 시작과 끝을 인식할 수 있게 한다.
- 분해된 토큰은 N-gram을 사용하여 하위 단어 집합으로 분해한다.
- 분해된 하위 단어 집합에는 나눠지지 않은 토큰 자체도 포함한다. 이렇게 하위 단어 집합이 만들어지면, 각 하위 단어는 고유한 벡터값을 갖는다.
일반적으로 fastText는 다양한 N-gram을 적용해 입력 토큰을 분해하고 하위 단어 벡터를 구성함으로써 단어의 부분 문자열을 고려하는 유연하고 정확한 하위 단어 집합을 생성한다.
즉, 같은 하위 단어를 공유하는 단어끼리는 정보를 공유해 학습할 수 있으므로 비슷한 단어끼리는 비슷한 임베딩 벡터를 갖게 되어, 단어 간 유사도를 높일 수 있다.
또한 OOV 단어도 하위 단어로 나누어 임베딩을 계산할 수 있게 된다.
이렇게 하위 단어 기반의 임베딩 방법을 사용하면, 말뭉치에 등장하지 않은 단어라도 유사한 하위 단어를 가지고 있으면 유사한 임베딩 벡터를 갖게 된다.
모델 실습
fastText 모델은 CBoW와 Skip-gram으로 구성되며 네거티브 샘플링 기법을 사용해 학습한다. 단 fastText는 하위 단어로 학습한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Fast 클래스
fasttext = gensim.models.FastText(
sentences=None,
corpus_file=None,
vector_size=100,
alpha=0.025,
window=5,
min_count=5,
workers=3,
sg=0,
hs=0,
cbow_mean=1,
negative=5,
ns_exponent=0.75,
max_final_vocab=None,
epochs=5,
batch_words=10000,
min_n=3,
max_n=6
)
- 대부분은
Word2Vec클래스의 하이퍼파라미터와 동일 - N-gram 범위를 결정하는 하이퍼파라미터 추가
- 최소 N(
min_n): N-gram의 최솟값 - 최대 N(
max_n): N-gram의 최댓값
- 최소 N(
1
2
3
4
5
6
# KorNLI 데이터세트 전처리
from Korpora import Korpora
corpus = Korpora.load("kornli")
corous_texts = corpus.get_all_text() + corpus.get_all_pairs()
tokens = [sentence.split() for sentence in corpus_texts]
fastText 모델은 입력 단어의 구조적 특징을 학습할 수 있어 형태소 분석기를 통해 토큰화하지 않고 띄어쓰기를 기준으로 단어를 토큰화해 학습을 진행한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# fastText 모델
from gensim.models import FastText
fastText = FastText(
sentences=tokens,
vector_size=128,
window=5,
min_count=5,
sg=1,
epochs=3,
min_n=2,
max_n=6
)
# 모델 저장
fastText.save("../models/fastText.model")
# 모델 불러오기
fastText = FastText.load("../models/fastText.model")
Word2Vec 모델과는 다르게 OOV 단어를 대상으로도 의미 있는 임베딩을 추출할 수 있다.
1
2
3
4
5
6
7
8
# fastText OOV 처리
oov_token = "사랑해요"
oov_vector = fastText.wv[oov_token]
print(oov_token in fastText.wv.index_to_key)
print(fastText.wv.most_similar(oov_vector, topn=5))
# False
# [('사랑', 0.8812), ('사랑해', 0.8438), ('사랑의', 0.7931), ('사랑을', 0.75945), ('사랑하는', 0.750636)]
- Word2Vec 모델은 단어 사전에 존재하지 않는 단어의 임베딩 계산이 불가능했지만 fastText는 하위 단어로 나뉘어 있기 때문에 단어를 처리할 수 있다.
즉, fastText는 OOV 문제를 효과적으로 해결할 수 있으며 한국어와 같은 많은 언어는 형태적 구조를 갖고 있기 때문에 효과적 처리가 가능하다.