Python으로 딥러닝하기|자연어 2. 단어 임베딩, Word2Vec
- Paper
- References
- Word2Vec : https://wikidocs.net/22660
- Natural Language Processing: https://web.stanford.edu/class/cs224n/readings/cs224n-2019-notes01-wordvecs1.pdf
- Word2Vec 학습 방식: https://ratsgo.github.io/from%20frequency%20to%20semantics/2017/03/30/word2vec/
- 쉽게 쓰여진 word2vec: https://dreamgonfly.github.io/blog/word2vec-explained/
[사전학습] NLP Basic
: 텍스트 기반의 모델을 만들기 위해서는 우선 사람의 언어인 텍스트를 모델이 이해할 수 있도록 숫자로 바꾸어야 한다.
Tokenizer란
언어적 특성을 반영해 단어를 수치화하기 위해 문장을 명사, 동사, 형용사, 조사 등 의미 단위로 나눈다.
Python으로 딥러닝하기 | 자연어 처리 1. 토크나이징
Word Embedding이란
단어를 벡터로 바꾸는 모델을 Word embedding model이라고 부른다.
Frequency based 과 Prediction based를 각각 다루고자 했으나 아래의 블로그를 참고..
*참고: 빈도수 세기의 놀라운 마법 Word2Vec, Glove, Fasttext
- Sparse Representation
ex) one-hot vector: 표현하고자 하는 단어의 인덱스 값만 1, 나머지 인덱스는 전부 0으로 표현되는 방법 → "localist" representation
한계: 단어와 단어 간의 관계가 전혀 드러나지 않아 벡터에 단어의 의미를 담을 수 없다.
벡터 또는 행렬(matrix)의 값이 대부분이 0으로 표현되는 방법을 희소 표현(sparse representation)이라고 한다.
- Dense(distributed) Representation
분포 가설 가정: **'비슷한 위치(문맥)에서 등장하는 단어들은 비슷한 의미를 가진다'**라는 가정한다.한계: '단어 동시 등장 정보'를 가정하는 방법의 경우 의미상 아무런 관련이 없어 보이는 단어임에도 벡터공간에 매우 가깝게 임베딩되는 사례가 발생하곤 한다.
장점: 단어 벡터가 단어 간 유사도(의미)를 반영한 값을 가진다.
분산 표현(distributed representation) 방법은 단어의 의미를 여러차원에 분산하여 표현하는 방법이며 기본적으로 분포 가설(distributional hypothesis)이라는 가정 하에 만들어진 표현법이다.
*참고: NNLM
*참고: SVD
Word2Vec이란
Dense representation 학습 방법론 중 하나로 말 그대로 "단어를 vector로 바꾸는" 방법을 제안한 임베딩 모델이다.
- 2 algorithms: continuous bag-of-words (CBOW) and skip-gram.
- 2 training methods: negative sampling and hierarchical softmax.
- 학습원리(요약):
Word2Vec은 window 내에 등장하지 않는 단어에 해당하는 벡터는 중심단어 벡터와 벡터공간상에서 멀어지게끔(내적값 줄이기), 등장하는 주변단어 벡터는 중심단어 벡터와 가까워지게끔(내적값 키우기) 한다.
* dot products(내적): similarity(유사도) metric
* 내적은 코사인이므로 내적값 상향은 유사도를 높이는 의미!
2 algorithms
CBOW는 context(주변 단어)들을 가지고 현재의 단어를 예측하는 방법이고 반대로 Skip-gram은 현재의 단어로 주변의 단어를 예측하는 방법이다.
전반적으로 Skip-gram이 CBOW보다 성능이 좋다고 알려져 있고 많이 사용되기 때문에 Skip-gram에 대해 알아보겠다. 매커니즘 자체는 거의 동일하기 때문에 Skip-Gram을 이해하면 CBOW도 손쉽게 이해할 수 있다.
Skip-gram
Predicting surrounding context words given a center word
-
- input: target word
- output: context word
- window size: target 단어를 기준으로 참고할 양 옆 주변 단어의 개수 (한번에 학습할 단어 개수)
- sliding window : window를 움직여서 target 단어와 주변 단어를 바꿔가며 데이터셋을 만듬Skip-Gram dataset 구성 예시
- [그림출처]https://www.tensorflow.org/tutorials/text/word2vec
- 구조
[그림출처]https://web.stanford.edu/class/cs224n/readings/cs224n-2019-notes01-wordvecs1.pdf
-
- Input: target 단어의 one-hot vector
- Lable($Y$): context 단어의 one-hot vector
- Hidden layer:
- Word2Vec은 입력층과 출력층 사이에 하나의 은닉층(hidden layer)이 존재한다. (얕은 신경망)
- Size(dimension): 가장 적합한 사이즈는 데이터마다 다르며 경험적으로 결정해야한다. 일반적으로 데이터가 많을 수록 더 큰 크기로 한다. 단 입력층보다 작아야 필요한 정보를 간결하게 담을 수 있다.
- Output($\hat{Y}$): 예측한 단어 vector
- 훈련: Output에서 "Softmax"를 취한다음 ($\hat{Y}$)와 실제 label($Y$)벡터의 오차(Cross-entropy 함수를 통해 구함)를 최소화 하는 방법으로 학습한다.
CBOW(Continuous Bag of Words)
Predicting a center word from the surrounding context
- 구조
hidden layer에서 입력 벡터의 평균을 구함 (skip-gram은 input 단어가 1개이기 때문에 평균을 구하는 과정이 없음)
Hidden Layer (= Projection layer)
hidden layer는 입력벡터와 은닉층을 이어주는 가중치행렬이며 동시에 Skip-gram을 통해 얻고자 하는 최종 결과물인 Word Embedding Vector Table(사실은 transpose해야하기 때문에 서로 다른 행렬)이기도 하다.
ex) 단어 5개 one-hot-encoding된 입력벡터를 이용하여 Word2Vec을 수행한다고 가정
: hidden layer의 가중치 행렬 W에서 input 단어에 해당하는 index번째 행 벡터를 그대로 읽어오는(lookup) 방식과 동일하다.
This means that the hidden layer of this model is really just operating as a lookup table. The output of the hidden layer is just the “word vector” for the input word.
2 training methods
Negative Sampling
네거티브 샘플링은 Word2Vec이 학습 과정에서 전체 단어 집합이 아니라 일부 단어 집합에만 집중할 수 있도록 하는 방법이다.
context 단어를 positive, target 단어와 window size 안에 포함되지 않은 랜덤 샘플링된 단어를 negative로 labeling하여 다중 클래스 분류 문제를 이진 분류 문제로 풀도록 하는 방법으로 연산량에 훨씬 효율적이다.
Lable($Y$): positive | negative
훈련: Output에서 "Sigmoid"를 취한다음 ($\hat{Y}$)와 실제 label($Y$)벡터의 오차(Cross-entropy 함수를 통해 구함)를 최소화 하는 방법으로 학습한다.
Hierarchical Softmax
모든 어휘에 대한 확률을 계산하기 위해 softmax에 비해 더 효율적인 트리 구조를 사용하여 단어를 정의한다.
계층적 소프트맥스는 Binary tree를 사용하여 vocabulary 의 모든 단어를 나타낸다. 나무의 잎사귀 하나하나가 하나의 단어이며, 뿌리에서 잎까지 고유한 경로가 있다.
단어를 잎사귀 하나하나에 할당하는 방식:
ex Fig 4) w2에 vector 할당하기
root 노드에서 w2에 도달하기 위한 경로: left 2번한 다음 right 1번
$P(w_2|w_i) = p(n(w_2, 1), left) · p(n(w_2, 2), left) · p(n(w_2, 3), right)$
이 모델은 word representation을 output으로 가져올 수 없고, 대신 그래프의 각 노드(루트와 잎 제외)는 모델이 학습할 벡터와 연결된다.
논문에서는 자주 사용되는 단어에 더 짧은 paths를 주는 binary Huffman tree를 사용하였다.
In practice, hierarchical softmax tends to be better for infrequent words, while negative sampling works better for frequent words and lower dimensional vectors.
Skip-Gram with Negative Sampling(SGNS) Code Example
Word2Vec (tensorflow code) : https://www.tensorflow.org/tutorials/text/word2vec
Create Negative sampling dataset
# Generates skip-gram pairs with negative sampling for a list of sequences
# (int-encoded sentences) based on window size, number of negative samples
# and vocabulary size.
def generate_training_data(sequences, window_size, num_ns, vocab_size, seed):
# Elements of each training example are appended to these lists.
targets, contexts, labels = [], [], []
# Build the sampling table for vocab_size tokens.
sampling_table = tf.keras.preprocessing.sequence.make_sampling_table(vocab_size)
# Iterate over all sequences (sentences) in dataset.
for sequence in tqdm.tqdm(sequences):
# Generate positive skip-gram pairs for a sequence (sentence).
positive_skip_grams, _ = tf.keras.preprocessing.sequence.skipgrams(
sequence,
vocabulary_size=vocab_size,
sampling_table=sampling_table,
window_size=window_size,
negative_samples=0)
# Iterate over each positive skip-gram pair to produce training examples
# with positive context word and negative samples.
for target_word, context_word in positive_skip_grams:
context_class = tf.expand_dims(
tf.constant([context_word], dtype="int64"), 1)
negative_sampling_candidates, _, _ = tf.random.log_uniform_candidate_sampler(
true_classes=context_class,
num_true=1,
num_sampled=num_ns,
unique=True,
range_max=vocab_size,
seed=SEED,
name="negative_sampling")
# Build context and label vectors (for one target word)
negative_sampling_candidates = tf.expand_dims(
negative_sampling_candidates, 1)
context = tf.concat([context_class, negative_sampling_candidates], 0)
label = tf.constant([1] + [0]*num_ns, dtype="int64")
# Append each element from the training example to global lists.
targets.append(target_word)
contexts.append(context)
labels.append(label)
return targets, contexts, labels
Main Model with negative sampling
class Word2Vec(tf.keras.Model):
def __init__(self, vocab_size, embedding_dim):
super(Word2Vec, self).__init__()
## tf: 두 임베딩의 연결을 최종 Word2Vec 임베딩으로 사용할 수도 있습니다.
self.target_embedding = layers.Embedding(vocab_size,
embedding_dim,
input_length=1,
name="w2v_embedding")
self.context_embedding = layers.Embedding(vocab_size,
embedding_dim,
input_length=num_ns+1)
def call(self, pair):
target, context = pair
# target: (batch, dummy?) # The dummy axis doesn't exist in TF2.7+
# context: (batch, context)
if len(target.shape) == 2:
target = tf.squeeze(target, axis=1)
# target: (batch,)
word_emb = self.target_embedding(target)
# word_emb: (batch, embed)
context_emb = self.context_embedding(context)
# context_emb: (batch, context, embed)
## euni: target 단어와 context 단어의 내적을 수행하여 label을 예측
dots = tf.einsum('be,bce->bc', word_emb, context_emb)
# dots: (batch, context)
return dots
Loss
def custom_loss(x_logit, y_true):
## euni: label과의 오차로 역전파
return tf.nn.sigmoid_cross_entropy_with_logits(logits=x_logit, labels=y_true)
Word2Vec Train
## euni: 1. Sample Training Dataset
path_to_file = tf.keras.utils.get_file('shakespeare.txt', 'https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt')
text_ds = tf.data.TextLineDataset(path_to_file).filter(lambda x: tf.cast(tf.strings.length(x), bool))
## euni: 2. tokenizer
# Now, create a custom standardization function to lowercase the text and
# remove punctuation.
def custom_standardization(input_data):
lowercase = tf.strings.lower(input_data)
return tf.strings.regex_replace(lowercase,
'[%s]' % re.escape(string.punctuation), '')
# Define the vocabulary size and number of words in a sequence.
vocab_size = 4096
sequence_length = 10
# Use the TextVectorization layer to normalize, split, and map strings to
# integers. Set output_sequence_length length to pad all samples to same length.
vectorize_layer = layers.TextVectorization(
standardize=custom_standardization,
max_tokens=vocab_size,
output_mode='int',
output_sequence_length=sequence_length)
vectorize_layer.adapt(text_ds.batch(1024))
text_vector_ds = text_ds.batch(1024).prefetch(AUTOTUNE).map(vectorize_layer).unbatch()
sequences = list(text_vector_ds.as_numpy_iterator())
## euni: 3. train dataset 구성 (negative sampling)
targets, contexts, labels = generate_training_data(
sequences=sequences,
window_size=2,
num_ns=4,
vocab_size=vocab_size,
seed=SEED)
BATCH_SIZE = 1024
BUFFER_SIZE = 10000
dataset = tf.data.Dataset.from_tensor_slices(((targets, contexts), labels))
dataset = dataset.shuffle(BUFFER_SIZE).batch(BATCH_SIZE, drop_remainder=True)
dataset = dataset.cache().prefetch(buffer_size=AUTOTUNE)
## euni: 4. embedding model train
embedding_dim = 128
word2vec = Word2Vec(vocab_size, embedding_dim)
word2vec.compile(optimizer='adam',
loss=tf.keras.losses.CategoricalCrossentropy(from_logits=True),
metrics=['accuracy'])
word2vec.fit(dataset, epochs=20)
Word2Vec Embedding
weights = word2vec.get_layer('w2v_embedding').get_weights()[0]
vocab = vectorize_layer.get_vocabulary()
# index: vectorize vocabulary index
for index, word in enumerate(vocab):
if index == 0:
continue # skip 0, it's padding.
vec = weights[index]