본문 바로가기

책/파이토치 트랜스포머를 활용한 자연어 처리와 컴퓨터비전 심층학습

06 임베딩 (1) N-gram, TF-IDF, Word2Vec, fastText

토큰화만으로는 모델을 학습할 수 없기 때문에 텍스트를 숫자로 변환하는 텍스트 벡터화 과정이 필요하다. 기초적인 텍스트 벡터화로는 원-핫 인코딩과, 빈도 벡터화 등이 있다.

 

그러나 원-핫 인코딩은 벡터의 sparsity가 크다는 단점이 있고, 둘 다 텍스트의 벡터가 입력 텍스트의 의미를 내포하고 있지 않다는 단점이 있다.

 

이를 위해 Word2Vec나 fastText 등과 같이 단어의 의미를 학습해 표현하는 워드 임베딩(Word Embedding) 기법을 사용한다. 워드 임베딩은 단어를 고정된 길이의 실수 벡터로 표현하는 방법으로, 단어의 의미를 벡터 공간에서 다른 단어와의 상대적인 위치로 표현해 단어 간의 관계를 추론한다.

 

워드 임베딩은 고정된 임베딩을 학습하기 때문에 다의어나 문맥 정보를 다루기 어렵다는 단점이 있어 인공 신경망을 활용해 동적 임베딩(Dynamic Embedding) 기법을 사용하기도 한다.

 

언어 모델

언어 모델(Language Model)이란 입력된 문장으로 각 문장을 생성할 수 있는 확률을 계산하는 모델이다. 예를 들어 입력으로 "안녕하세요"가 들어온 경우 "만나서 반갑습니다"가 나올 확률은 0.1, "미국의 수도는 워싱턴입니다"가 나올 확률은 0.0000001과 같은 경우이다.

 

그렇지만 주어진 문장 뒤에 나올 수 있는 문장은 매우 다양하기 때문에 완성된 문장 단위로 확률을 계산한느 것은 어려운 일이다. 이러한 문제를 해결하기 위해 문장 전체를 예측하는 방법 대신에 하나의 토큰 단위로 예측하는 방법인 자기회귀 언어 모델이 고안됐다.

 

자기회귀 언어 모델

자기회귀 언어 모델(Autoregressive Language Model)은 입력된 문장들의 조건부 확률을 이용해 다음에 올 단어를 예측한다.

 

이를 위해 이전에 등장한 모든 토큰의 정보를 고려하여, 문장의 문맥 정보를 파악하여 다음 단어를 생성한다. 수식으로 표현하면 다음과 같다.

 

$P(w_t|w_1, w_2, ..., w_{t-1})$

 

통계적 언어 모델

통계적 언어 모델(Statistical Language Model)은 마르코프 체인을 이용해 구현된다. 마르코프 체인은 빈도 기반의 조건부 확률 모델 중 하나로 이전 상태와 현재 상태 간의 전이 확률을 이용해 다음 상태를 예측한다.

 

예를 들어 말뭉치에 '안녕하세요'라는 문장이 1000번 등장하고 이어서 '안녕하세요 만나서'가 700번, '안녕하세요 반갑습니다'가 100번 등장했다고 가정한다면 빈도의 기반 조건부 확률 수식은 다음과 같다.

 

$P(만나서|안녕하세요) = {P(안녕하세요 만나서) \over P(안녕하세요)} = {700 \over 1000}$

 

$P(반갑습니다|안녕하세요) = {P(안녕하세요 반갑습니다) \over P(안녕하세요)} = {100 \over 1000}$

 

이 방법은 단어의 순서와 빈도에만 기초하므로 문맥을 제대로 파악하지 못하면 부적절한 결과를 생성할 수 있고, 한 번도 등장한 적이 없는 단어만 문장에 대해서는 정확한 확률을 예측하기가 어렵다.

 

그럼에도 불구하고 통계적 언어 모델은 대규모 자연어 데이터를 처리하는데 효과적이며, 기존에 학습한 텍스트 데이터에서 패턴을 찾아 확률 분포를 생성하므로, 이를 이용해 새로운 문장을 생성할 수 있으며, 다양한 종류의 텍스트 데이터를 학습할 수 있다.

 

N-gram

가장 기초적인 통계적 언어 모델이다. 텍스트에서 N개의 연속된 단어 시퀀스를 하나의 단위로 취급하여 특정 단어 시퀀스가 등장할 확률을 추정한다.

 

N이 1일 때는 Unigram, 2일 때는 Bigram,  3일 때는 Trigram으로 부르고 4 이상이면 N-gram으로 부른다.

 

N-gram 구현 파이썬 코드

def ngrams(sentence, n):
  words = sentence.split()
  ngrams = zip(*[words[i:] for i in range(n)])
  return list(ngrams)

sentence = "안녕하세요 만나서 진심으로 반가워요"

unigram = ngrams(sentence, 1)
bigram = ngrams(sentence, 2)
trigram = ngrams(sentence, 3)

print(unigram)
print(bigram)
print(trigram)
[('안녕하세요',), ('만나서',), ('진심으로',), ('반가워요',)]
[('안녕하세요', '만나서'), ('만나서', '진심으로'), ('진심으로', '반가워요')]
[('안녕하세요', '만나서', '진심으로'), ('만나서', '진심으로', '반가워요')]

 

N-gram 구현 NLTK 활용

import nltk

unigram = nltk.ngrams(sentence.split(), 1)
bigram = nltk.ngrams(sentence.split(), 2)
trigram = nltk.ngrams(sentence.split(), 3)

print(list(unigram))
print(list(bigram))
print(list(trigram))
[('안녕하세요',), ('만나서',), ('진심으로',), ('반가워요',)]
[('안녕하세요', '만나서'), ('만나서', '진심으로'), ('진심으로', '반가워요')]
[('안녕하세요', '만나서', '진심으로'), ('만나서', '진심으로', '반가워요')]
  • 작은 규모의 데이터세트에서 연속된 문자열 패턴을 분석하는 데 큰 효과를 보인다.
  • 예를 들어 '입이 무겁다'라는 표현 처럼 자주 등장하는 연속된 단어나 구를 추출하고, 이를 분석함으로써 관용적 표현을 파악할 수 있다.
  • 단어의 순서가 중요한 자연어 처리 작업 및 문자열 패턴 분석에 활용된다.

 

TF-IDF

TF-IDF(Term Frequency-Inverse Document Frequency)란 텍스트 문서에서 특정 단어의 중요도를 계산하는 방법으로, 문서 내에서 단어의 중요도를 평가하는데 사용되는 통계적인 가중치를 의미한다.

 

단어 빈도

단어 빈도(Term Frequency, TF)란 문서 내에서 특정 단어의 빈도수를 나타내는 값이다. 예를 들어 3개의 문서에서 'movie'라는 단어가 4번 등장한다면 해당 단어의 TF 값은 4가 된다.

 

문서 빈도

문서 빈도(Document Frequency, DF)란 한 단어가 얼마나 많은 문서에 나타나는지를 의미한다. 3개의 문서에서 'movie'라는 단어가 4번 등장한다면 해당 단어의 DF 값은 3이 된다.

 

역문서 빈도

역문서 빈도(Inverse Document Frequency, IDF)란 전체 문서 수를 문서 빈도로 나눈 다음에 로그를 취한 값을 말한다. 이는 문서 내에서 특정 단어가 얼마나 중요한지를 나타낸다.

 

문서 빈도가 높을수록 해당 단어가 일반적이라는 의미가 된다. 그러므로 문서 빈도의 역수를 취하면 단어의 빈도수가 적을수록 IDF 값이 커지게 보정하는 역할을 한다. IDF를 수식으로 표현하면 다음과 같다.

 

$IDF(t, D) = {\log {count(D) \over 1 + DF(t, D)}}$

 

TF-IDF

TF-IDF는 문서 빈도와 역문서 빈도를 곱한 값으로 사용한다.

 

$TF-IDF(t, d,D) = TF(t, d) \times IDF(t, d)$

 

문서 내에 단어가 자주 등장하지만, 전체 문서 내에 해당 단어가 적게 등장한다면 TF-IDF 값은 커진다. 그러므로 전체 문서에서 자주 등장할 확률이 높은 관사나 관용어 등의 가중치는 낮아진다.

 

TF-IDF 계산

from sklearn.feature_extraction.text import TfidfVectorizer


corpus = [
    "That movie is famous movie",
    "I like that actor",
    "I don’t like that actor"
]

tfidf_vectorizer = TfidfVectorizer()
tfidf_vectorizer.fit(corpus)
tfidf_matrix = tfidf_vectorizer.transform(corpus)
# tfidf_matrix = tfidf_vectorizer.fit_transform(corpus)

print(tfidf_matrix.toarray())
print(tfidf_vectorizer.vocabulary_)
[[0.         0.         0.39687454 0.39687454 0.         0.79374908
  0.2344005 ]
 [0.61980538 0.         0.         0.         0.61980538 0.
  0.48133417]
 [0.4804584  0.63174505 0.         0.         0.4804584  0.
  0.37311881]]
{'that': 6, 'movie': 5, 'is': 3, 'famous': 2, 'like': 4, 'actor': 0, 'don': 1}
  • 문서마다 중요한 단어만 추출할 수 있으며, 벡터값을 활용해 문서 내 핵심 단어를 추출할 수 있다.
  • 빈도기반 덱터화는 문자으이 순서나 문맥을 고려하지 않는다. 그러므로 문장 생성과 같이 순서가 중요한 작업에는 부적합하다.
  • 벡터가 단어의 의미를 담고 있지는 않다.

 

Word2Vec

Word2Vec은 분포 가설을 기반으로 개발됐다. 분포 가설이란 단어의 의미는 주변 단어에 의해 형성된다는 아이디어인데, 단어 자체에는 의미가 없고 그 단어가 사용된 맥락이 의미를 형성한다는 것이다.

 

자세한 내용은 이전 포스팅을 참고

2023.09.24 - [책/밑바닥부터 시작하는 딥러닝 2] - 2장 자연어와 단어의 분산 표현

2023.09.25 - [책/밑바닥부터 시작하는 딥러닝 2] - 3장 word2vec

2023.09.26 - [책/밑바닥부터 시작하는 딥러닝 2] - 4장 word2vec 속도 개선

 

모델 실습 : Skip-gram

Word2Vec 모델은 학습할 단어의 수를 V, 임베딩 차원을 E로 설정해 $W_{V \times E}$ 행렬과 $ {W'}_{E \times V}$ 행렬을 최적화하며 학습한다. 이때 $W_{V \times E}$ 행렬은 배열이나 리스트 등의 데이터 구조에서 인덱스를 이용해 해당하는 값을 찾아오는 연산인 룩업(Lookup) 연산을 수행하는데, 임베딩(Embedding) 클래스를 사용하면 간편하게 구현할 수 있다.

 

임베딩 클래스는 단어나 범주형 변수와 같은 이산 변수를 연속적인 벡터 형태로 변환해 사용할 수 있다.

 

임베딩 클래스

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으로 설정
  • max_norm : 임베딩 벡터의 최대 크기를 지정, 임베딩 벡터의 크기가 max_norm 이상이면 벡터를 잘라내고 크기를 감소시킴

기본 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

 

영화 리뷰 데이터세트 전처리

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]
print(tokens[:3])
[['굳', 'ㅋ'], ['GDNTOPCLASSINTHECLUB'], ['뭐', '야', '이', '평점', '들', '은', '....', '나쁘진', '않지만', '10', '점', '짜', '리', '는', '더', '더욱', '아니잖아']]

 

단어 사전 구축

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(tokens, 5000, ['<unk>'])
token_to_id = {token: idx for idx, token in enumerate(vocab)}
id_to_token = {idx: token for idx, token in enumerate(vocab)}

print(vocab[:10])
print(len(vocab))
['<unk>', '.', '이', '영화', '의', '..', '가', '에', '...', '을']
5001

 

Skip-gram의 단어 쌍 추출

def get_word_pairs(tokens, window_size):
  pairs = []
  for sentence in tokens:
    sentence_length = len(sentence)
    for idx, center_word in enumerate(sentence):
      window_start = max(0, idx - window_size)
      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)
print(word_pairs[:5])
[['굳', 'ㅋ'], ['ㅋ', '굳'], ['뭐', '야'], ['뭐', '이'], ['야', '뭐']]
  • 중심 단어와 앞뒤 단어를 하나의 쌍으로 추출한다.

단어 쌍을 인덱스 쌍으로 변환

def get_index_pairs(word_pairs, token_to_id):
  pairs = []
  unk_index = token_to_id['<unk>']
  for word_pair in word_pairs:
    centor_word, context_word = word_pair
    centor_word_index = token_to_id.get(centor_word, unk_index)
    context_word_index = token_to_id.get(context_word, unk_index)
    pairs.append([centor_word_index, context_word_index])
  return pairs

index_pairs = get_index_pairs(word_pairs, token_to_id)
print(index_pairs[:5])
print(len(vocab))
[[595, 100], [100, 595], [77, 176], [77, 2], [176, 77]]
5001

 

학습 준비

import torch
from torch.utils.data import TensorDataset, DataLoader

index_pairs = torch.tensor(index_pairs)
center_indexs = index_pairs[:, 0]
context_indexs = index_pairs[:, 1]

dataset = TensorDataset(center_indexs, context_indexs)
dataloader = DataLoader(dataset, batch_size=256, shuffle=True)

from torch import optim

device = "cuda" if torch.cuda.is_available() else "cpu"
word2vec = VanillaSkipGram(vocab_size=len(token_to_id), embedding_dim=128).to(device)
criterion = nn.CrossEntropyLoss().to(device)
optimizer = optim.SGD(word2vec.parameters(), lr=0.1)

 

모델 학습

for epoch in range(20):
    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}")

 

임베딩 값 추출

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.45873535 -1.4125738  -1.3788592   0.7086098   1.2213398  -0.26372698
 -0.28037128 -1.4234469   1.5022029   1.2094473  -1.0158551   0.18843827
  0.50370085 -2.3361244  -0.06898285  0.9282817  -0.48920494  2.3391287
  1.9510529   1.0244179  -1.0589218   0.45513138 -0.26152292 -0.92398655
 -0.8618328  -0.23008227 -0.38347512  0.5492202   0.11639255  0.26918137
  1.3105102   0.93400407  0.02157224 -0.38650048 -1.4645252  -1.9102594
  0.7985533  -0.5641499  -0.67817944 -1.8232611   0.39260313  0.1452537
 -1.4572662   0.8155799   0.68955433  2.3247154  -2.3831      0.07159209
  1.9544561  -1.3041784   1.6265767  -0.47780564 -0.13338266 -1.2765781
  1.2211463  -0.54078066 -1.0523287  -0.11295447 -0.63868177 -0.6317604
  0.05026022 -1.5645266  -1.7714697   0.38536918  0.1453347   0.82799226
  0.5879392   1.2119329  -1.1189276  -1.6172574  -1.4835991  -1.2073246
  0.1977502   0.9653218  -0.16744262  1.2672088  -1.0045105   0.83066016
  0.93309015 -1.4094567  -0.04989136  1.165612   -2.3469994   0.48065534
  0.4089467   0.9083836   1.4834619   0.05953585  0.63869303 -1.077794
 -0.02494393 -0.6638253  -1.1920466  -0.16212064 -1.0870461   0.01633668
  0.8580365  -0.70808744 -0.69874567 -0.43113938 -1.0387203  -1.9231895
 -0.17656337 -0.4171057  -0.04092513 -1.8552705   0.8653499   0.6147957
  0.35898766  0.79467636  0.8459003   0.70341015 -0.08004533 -1.2992288
 -0.6225215   0.6057202   0.6025012   0.79180825 -0.32311577  0.7279106
 -0.99680936 -0.80647945 -0.31687465 -1.1623919   1.0356776   0.44287938
 -0.3521926   0.40414435]

 

단어 임베딩 코사인 유사도 계산

import numpy as np
from numpy.linalg import norm

def cosine_similarity(a, b):
  # a : token_embedding
  # b : embedding_matrix
  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 = cosine_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.3358
배우 - 유사도 : 0.3080
시나리오 - 유사도 : 0.3048
악마 - 유사도 : 0.2952
까지도 - 유사도 : 0.2937

 

모델 실습: Gensim

간단한 Skip-gram 모델을 학습할 때 데이터 수가 적은 경우에도 학습하는 데 오랜 시간이 소요된다. 이러한 경우 계층적 소프트맥스나 네거티브 샘플링 같은 기법을 사용하면 더 효율적으로 학습할 수 있다.

 

Gensim 라이브러리는 대용량 텍스트 데이터의 처리를 위한 메모리 효류적인 방법을 제공해 대규모 데이터세트에서도 효과적으로 모델을 학습할 수 있다.

 

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
)

# word2vec.save("../models/word2vec.model")
# word2vec = Word2Vec.load("../models/word2vec.model")

 

임베딩 추출 및 유사도 계산

word = "연기"
print(word2vec.wv[word])
print(word2vec.wv.most_similar(word, topn=5))
print(word2vec.wv.similarity(w1=word, w2="연기력"))
[-4.93290693e-01 -2.84351289e-01  3.69010538e-01  3.36962819e-01
 -9.64935124e-02 -5.49013987e-02 -4.85488214e-02 -1.27716467e-01
 -4.83507395e-01  3.21379542e-01  3.13959308e-02 -6.15233421e-01
 -2.92426646e-01  1.64555550e-01  3.66918594e-02 -8.56545269e-02
 -4.61444676e-01  3.18951048e-02 -1.03940899e-02  2.39758268e-01
  6.29726827e-01  4.55769122e-01 -1.13298431e-01 -9.43350568e-02
 -1.88041314e-01 -1.82925742e-02 -3.41772169e-01  1.44763231e-01
  1.03929475e-01 -1.62047133e-01 -3.31691086e-01 -6.38810918e-02
  2.35159218e-01 -1.64621472e-01  8.18434730e-02 -3.45117805e-05
  1.10119991e-02 -1.04571640e-01 -2.07111433e-01 -3.48203599e-01
  1.55132841e-02 -9.32942927e-02 -3.38449746e-01 -5.04595220e-01
 -2.27043867e-01  3.99443597e-01 -2.41629705e-01 -1.28127426e-01
  3.01755011e-01  8.97378400e-02  4.04503345e-01  2.81150848e-01
  2.06287652e-01  2.97228843e-01 -1.80032998e-01 -2.71678925e-01
 -2.24731162e-01  2.29277089e-01 -1.68268502e-01  2.22951025e-01
  5.27729876e-02 -1.21088989e-01  1.03757724e-01  3.59736569e-02
 -3.91371757e-01  9.36562847e-03  3.10790911e-02  2.94032753e-01
  3.91662449e-01 -3.29003572e-01 -4.48686063e-01 -3.47319096e-01
 -4.01768833e-01  6.67317659e-02 -7.32762888e-02 -2.12015480e-01
 -3.20789188e-01 -3.53328288e-01 -1.83699280e-01  9.93791595e-02
 -2.54023015e-01  3.87015976e-02  3.45913082e-01  7.76938021e-01
  4.97773200e-01  2.96262372e-02  3.85883182e-01 -4.51796770e-01
  2.37966701e-01  2.80202050e-02 -3.11047733e-01  1.96062271e-02
  2.97472239e-01  6.21288419e-01  3.94716188e-02  9.89301726e-02
 -2.22948164e-01 -5.74286468e-02 -2.38115683e-01 -6.46874070e-01
 -3.92658383e-01  1.74469166e-02  2.22188652e-01 -2.91427612e-01
  2.04873830e-01  4.87469971e-01  2.48682186e-01  1.73744127e-01
  5.57374395e-02 -3.58898371e-01  4.84102249e-01 -2.94970065e-01
 -1.65744528e-01  3.76391351e-01 -4.85834405e-02 -3.33001852e-01
  2.99326003e-01  1.78990975e-01 -9.90194976e-02  4.77505296e-01
 -3.92675884e-02 -1.67990878e-01  1.05300441e-01  5.53033017e-02
  2.69727204e-02 -2.42120504e-01 -4.65785593e-01 -8.79928917e-02]
[('연기력', 0.799852728843689), ('캐스팅', 0.7382062077522278), ('조연', 0.7178145051002502), ('목소리', 0.7166693806648254), ('연기자', 0.7116499543190002)]
0.79985267

 

Word2Vec은 분포 가설을 통해 쉽고 빠르게 단어의 임베딩을 학습할 수 있지만, 이는 단어의 형태학적 특징을 반영하지 못한다는 한계가 있다.

 

예를 들어 한국어는 어근과 접사, 조사 등으로 이루어지는 규칙을 가지고 있기 때문에 Word2Vec 모델에서는 이러한 구조적 특징을 제대로 학습하기가 어렵다. 이러한 한계는 제한된 단어 사전에서 많은 OOV를 발생시키는 원인이 된다.

 

 

fastText

fastText는 단어의 하위 단어를 고려하기위해 N-gram을 사용해 단어를 분해하고 벡터화하는 방법으로 동작한다.

 

  • 입력단어 : 서울특별시
  • <, > 더하기 : <서울특별시>
  • N-gram 분해 : <서울, 서울특, 울특별, 특별시, 별시>
  • 전체 단어 추가 : <서울, 서울특, 울특별, 특별시, 별시>, <서울특별시>

fastText는 위와 같은 방법으로 하위 단어 집합을 생성한다. 각 하위 단어의 임베딩 벡터를 구하고, 이를 모두 합산하여 입력 단어의 최종 임베딩 벡터를 계산한다.

 

이러한 방법의 장점은 OOV에 강력하게 대응할 수 있는데, 예를 들어 '개인택시', '정보처리기사', '임대차보호법'이라는 단어가 말뭉치 내에 있을 때, 이 단어들의 하위 단어들로부터 '개인정보보호법'이라는 단어의 임베딩도 연산할 수 있다.

 

모델 실습

corpus = Korpora.load("kornli")
corpus_texts = corpus.get_all_texts() + corpus.get_all_pairs()
tokens = [sentence.split() for sentence in corpus_texts]

print(tokens[:3])

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")

KorNLI 데이터세트는 한국어 자연어 추론을 위한 데이터세트다. 장녀어 추론이란 두 개 이상의 문장이 주어졌을 때 두 문장 간의 관계를 분류하는 작업을 의미한다. 이를 통해 문장이 서로 밀접하게 연관되어 있는 함의 관계(entailment), 중립 관계(neutral), 불일치 관계(contradiction) 중 어느 관계에 해당되는지 분류할 수 있다.

 

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.9106492400169373), ('사랑', 0.8731779456138611), ('사랑한', 0.8662685751914978), ('사랑해서', 0.8453728556632996), ('사랑해.', 0.8408307433128357)]
  • "사랑해요"라는 단어는 단어 사전에 존재하지 않는 단어이지만, '사랑', '랑해', '해요'의 하위 단어로 분해되어 다른 단어에서 등장했던 '사랑'과 같은 하위 단어를 통해 '사랑해요' 토큰의 임베딩을 계산할 수 있다.