코딩을 하기에 앞서서 볼만한 동영상 추천이 있어서 해당 내용을 기재하고 진행
김성훈 교수님 Lecture 12 NN의 꽃 RNN 이야기
RNN은 이전의 문맥(state)를 가진 상태에서 다음 값의 영향을 주기 위해 탄생한 것
Series 데이터에 적합한 형태
이전의 state를 가진 h와 x의 연산을 통해 새로운 state를 가진 h를 만들어낸다
state 식의 형식을 WX의 형태로 나타낸 식
좌측과 우측의 식을 같게 표현하는 이유는 가중치는 동일한 상태에서 똑같은 구조로 state만 추가하며 가져가기 때문
hello라는 단어를 통해 예제로 입력했을 때 RNN 내부에서 단어가 어떻게 들어가는지 보여주는 예제
네이버 검색어 예제로 했을 때 홍콩이라는 단어를 입력하면 김성훈이 연관검색어로 나온다는 예를 들어 설명을 해주심
Vocab을 일종의 label이라고 한 부분이 와닿은 부분, 원 핫 인코딩의 예제
input layer에 W_xh 가중치를 곱하여 가상으로 나온 hidden layer의 값이 나온다
hidden layer의 값은 다음 단어의 결과가 나올때 + 연산을 통해 영향을 미친다
각 hidden layer에 있는 값에 W_hy 연산을 통해서 output layer를 출력하는데 이땐 input layer와 같은 수의 값이 나온다
output layer에 softmax 함수를 취하고, cost function을 활용하여 학습을 한다
RNN은 다양한 형태로 사용될 수 있는데
좌측부터 Vanilla Neural Networks, Image Captioning, Sentiment Classification, Machine Translation, Video classification의 역할들이 예시로 나온다
RNN에 대한 이론을 충전했겠다 Exploration을 진행
처음엔 조그마한 문장을 통해서 토큰화와 단어 인덱싱을 배웁니다
sentences = ['i feel hungry', 'i eat lunch', 'now i feel happy']
word_list = 'i feel hungry'.split()
# Data dictionary
words = ['<PAD>','<BOS>','<UNK>','i','feel','hungry','eat','lunch','now','happy']
index_to_word = {i:words[i] for i in range(len(words))}
index_to_word
#result
{0: '<PAD>',
1: '<BOS>',
2: '<UNK>',
3: 'i',
4: 'feel',
5: 'hungry',
6: 'eat',
7: 'lunch',
8: 'now',
9: 'happy'}
반대로 단어에서 인덱스로 바꾸는 과정이 필요한데 이는 간단하게 뒤집기만 하면 된다
word_to_index = {word:index for index, word in index_to_word.items()}
word_to_index['feel']
#result
4
앞으로 반복적으로 하게 될 일이므로 미리 함수화!
def get_encoded_sentence(sentence, word_to_index):
return [word_to_index['<BOS>']] + [word_to_index[word] if word in word_to_index else word_to_index['<UNK>'] for word in sentence.split()]
print(get_encoded_sentence('i eat lunch', word_to_index))
#result
[1, 3, 6, 7]
# 위 함수명과 s하나 차이 주의
def get_encoded_sentences(sentences, word_to_index):
return [get_encoded_sentence(sentence, word_to_index) for sentence in sentences]
encoded_sentences = get_encoded_sentences(sentences, word_to_index)
encoded_sentences
#result
[[1, 3, 4, 5], [1, 3, 6, 7], [1, 8, 3, 4, 9]]
인코딩이 있으면 디코딩 과정 또한 있어야 사람이 볼 수 있겠죠!
def get_decoded_sentence(encoded_sentence, index_to_word):
return ' '.join(index_to_word[index] if index in index_to_word else '<UNK>' for index in encoded_sentence[1:])
print(get_decoded_sentence([1, 3, 4, 5], index_to_word))
# result
i feel hungry
# 위 함수명과 s 하나차이 주의
def get_decoded_sentences(encoded_sentences, index_to_word):
return [get_decoded_sentence(sentence, index_to_word) for sentence in encoded_sentences]
print(get_decoded_sentences(encoded_sentences, index_to_word))
#result
['i feel hungry', 'i eat lunch', 'now i feel happy']
직접 만든 단어 목록을 텐서플로와 함께 임베딩 레이어로 만들 수 있습니다
import numpy as np
import tensorflow as tf
import os
vocab_size = len(word_to_index)
word_vector_dim = 4
embedding = tf.keras.layers.Embedding(input_dim=vocab_size, output_dim=word_vector_dim, mask_zero=True)
raw_inputs = np.array(get_encoded_sentences(sentences, word_to_index), dtype='object')
# add PAD in sentence to make same size sentence
raw_inputs = tf.keras.preprocessing.sequence.pad_sequences(raw_inputs,
value=word_to_index['<PAD>'],
padding='post',
maxlen=5)
output = embedding(raw_inputs)
print(output)
#result
...
[[ 0.03774967 -0.04962 0.01370635 0.02361702]
[ 0.04071461 -0.00313739 -0.03750011 -0.02407917]
[ 0.04074228 0.02750745 -0.01594061 -0.01592735]
[ 0.00011462 0.01852102 0.03945455 0.02238773]
[ 0.0448387 -0.00046003 -0.01173993 -0.00342659]]], shape=(3, 5, 4), dtype=float32)
단어의 임베딩이 끝나면 모델을 통해 학습을 시작할 수 있습니다 데이터를 넣을 때 input_shape에 대한 생각을 미처하지 못해서 오류를 많이 겪었는데, 해당 레이어에 따라 input형태를 잘 선택해야 합니다
vocab_size = 10
word_vector_dim = 4
model = tf.keras.Sequential()
model.add(tf.keras.layers.Embedding(vocab_size, word_vector_dim, input_shape=(None,)))
model.add(tf.keras.layers.LSTM(8)) # LSTM state 벡터의 차원수는 8
model.add(tf.keras.layers.Dense(8,activation='relu'))
model.add(tf.keras.layers.Dense(1, activation='sigmoid')) # 긍부정 1dim
model.summary()
# result
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
embedding_1 (Embedding) (None, None, 4) 40
_________________________________________________________________
lstm (LSTM) (None, 8) 416
_________________________________________________________________
dense (Dense) (None, 8) 72
_________________________________________________________________
dense_1 (Dense) (None, 1) 9
=================================================================
Total params: 537
Trainable params: 537
Non-trainable params: 0
_________________________________________________________________
테스트를 위한 간단한 모델로 학습까진 하지않고 넘어갔습니다 본편은 뒤에서부터 시작
IMDB Data set
50000개의 리뷰가 있고 반반씩 훈련용과 테스트용 데이터가 존재한다
텐서플로우를 활용하면 바로 활용할 수 있게끔 준비가 되어있다
imdb = tf.keras.datasets.imdb
(x_train, y_train), (x_test, y_test) = imdb.load_data(num_words=10000)
print(f"train sample: {len(x_train)}, test sample: {len(x_test)},")
#result
train sample: 25000, test sample: 25000,
아까 어렵사리 word index를 만들었던 것과는 달리 해당 객체에 메서드가 존재한다
word_to_index = imdb.get_word_index()
index_to_word = {index:word for word, index in word_to_index.items()}
print(index_to_word[1], word_to_index['the'])
#result
the 1
# 앞에 인덱스를 비워주기 위해 3을 더하는데 이는 단어 외에 token을 표시하기 위함이다
word_to_index = {k:(v+3) for k,v in word_to_index.items()}
word_to_index["<PAD>"] = 0
word_to_index["<BOS>"] = 1
word_to_index["<UNK>"] = 2
word_to_index['<UNUSED>'] = 3
index_to_word = {index:word for word, index in word_to_index.items()}
print(index_to_word[1], word_to_index['the'], index_to_word[4])
#result
<BOS> 4 the
하나의 샘플을 활용해서 테스트를 해본다
print(get_decoded_sentence(x_train[0], index_to_word))
print(y_train[0])
#result
~~because it was true and was someone's life after all that was shared with us all
1
아까는 문장의 길이는 임의의 숫자로 맞추었지만 이 숫자는 성능에 영향을 미치는 요소 중 하나이다
따라서 문장의 길이 또한 데이터에 맞춰서 변경됨이 바람직하다
혼자 개발할때는 어느정도 내용을 알고 있다보니 print를 다음과 같이 활용하지 않았는데, 나중에 코드를 쓰고 난뒤면 저런 print문이 없으면 다시 해석하는데 시간이 걸릴 수 있다, 그래서 이런 방식의 코드는 써먹기 좋다는 생각이 든다
total_data_text = list(x_train) + list(x_test)
num_tokens = [len(tokens) for tokens in total_data_text]
num_tokens = np.array(num_tokens)
print("문장길이 평균: ", np.mean(num_tokens))
print("문장길이 최대: ", np.max(num_tokens))
print("문장길이 표준편차 ", np.std(num_tokens))
max_tokens = np.mean(num_tokens) + 2 * np.std(num_tokens)
maxlen = int(max_tokens)
print('pad_sequences maxlen : ', maxlen)
print(f'전체 문장의 {np.sum(num_tokens < max_tokens) / len(num_tokens)}%가 maxlen 설정값 이내에 포함됩니다')
#result
문장길이 평균: 234.75892
문장길이 최대: 2494
문장길이 표준편차 172.91149458735703
pad_sequences maxlen : 580
전체 문장의 0.94536%가 maxlen 설정값 이내에 포함됩니다
각 문장의 길이를 맞추기 위한 PAD가 붙는 방향(앞, 뒤)도 중요한데 이는 특히 길이 길어질수록 RNN의 성능에 영향을 주기 때문에 가급적이면 앞에 붙는 것이 유리하다 padding='pre'
x_train = tf.keras.preprocessing.sequence.pad_sequences(x_train,
value=word_to_index["<PAD>"],
padding='pre',
maxlen=maxlen)
x_test = tf.keras.preprocessing.sequence.pad_sequences(x_test,
value=word_to_index["<PAD>"],
padding='pre',
maxlen=maxlen)
print(x_train.shape)
모델을 정의하는데 keras 튜토리얼에 있는 모델을 가져와서 시도해보고자 한다
vocab_size = 10000
word_vector_dim = 16
model = tf.keras.Sequential()
model.add(tf.keras.layers.Embedding(vocab_size, word_vector_dim, input_shape=(None,)))
model.add(tf.keras.layers.LSTM(16))
model.add(tf.keras.layers.Dense(64, activation='relu'))
model.add(tf.keras.layers.Dense(32, activation='relu'))
model.add(tf.keras.layers.Dense(1, activation='sigmoid'))
model.summary()
학습에 앞서서 데이터셋의 분리를 통해 valid 셋을 만든다
# validation 분리
x_val = x_train[:10000]
y_val = y_train[:10000]
# validation 제외 train set
partial_x_train = x_train[10000:]
partial_y_train = y_train[10000:]
print(partial_x_train.shape)
print(partial_y_train.shape)
#result
(15000, 580)
(15000,)
드디어 모델 학습 개시
model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy'])
epochs=20
history = model.fit(partial_x_train,
partial_y_train,
epochs=epochs,
batch_size=512,
validation_data=(x_val, y_val),
verbose=1)
results = model.evaluate(x_test, y_test, verbose=2)
print(results)
#result
782/782 - 5s - loss: 0.9932 - accuracy: 0.8336
[0.9931665062904358, 0.8335599899291992]
모델의 학습이 종료되면 학습간에 정확도나 손실에 대한 값들이 저장되어 있어서 이를 시각화 할 수 있다
단순 연습용이라 단촐하게 표시를 했는데 기왕이면 화살표등의 효과를 넣으면 더 잘 보일 듯 하다
import matplotlib.pyplot as plt
acc = history_dict['accuracy']
val_acc = history_dict['val_accuracy']
loss = history_dict['loss']
val_loss = history_dict['val_loss']
epochs = range(1, len(acc) +1)
plt.plot(epochs, loss, 'bo', label='Training loss') # bo = blue dot
plt.plot(epochs, val_loss, 'b', label='Validation loss') # b = blue line
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()
이렇게 학습이 완료된 모델의 파라미터는 저장해서 나중에 바로 불러올 수 있다
embedding_layer = model.layers[0]
weights = embedding_layer.get_weights()[0]
print(weights.shape)
#result
(10000, 16)
word2vec_file_path = os.path.join(os.getcwd(), 'data/word2vec.txt')
with open(word2vec_file_path, 'w') as f:
f.write(f'{vocab_size-4} {word_vector_dim}\n')
# 단어 개수 중 특수토큰 제외하고 워드 벡터를 파일에 기록
vectors = model.get_weights()[0]
for i in range(4, vocab_size):
f.write(f"{index_to_word[i]} {' '.join(map(str,list(vectors[i,:])))}\n")
Gensim
이렇게 저장되어있는 워드 벡터를 손쉽게 불러올 수 있도록 도와주는 라이브러리가 있다
단 주의할 것은 Gensim의 버전이 최근 4로 업그레이드 되었는데 이전과 많이 달라서 에러를 경험할 수 있다
포스팅에 사용된 Gensim의 버전은 3.8.3 버전이다
from gensim.models.keyedvectors import Word2VecKeyedVectors
word_vectors = Word2VecKeyedVectors.load_word2vec_format(word2vec_file_path, binary=False)
vector = word_vectors['computer']
vector
#result
array([-0.00633256, 0.0170492 , -0.03281455, 0.00792701, -0.06036439,
0.01141457, -0.04657213, -0.01924368, -0.02782674, -0.02220346,
-0.02408809, 0.01155608, 0.01127625, 0.03931998, -0.03906575,
0.00649046], dtype=float32)
워드 벡터에서 유사한 단어 찾기
워낙 추상적인 내용이고, 정답이 없는 부분이라 동의가 되는 부분과 안되는 부분이 있지만 어쨌거나 상당한 관련이 있는 단어들을 가져오는 것은 분명해 보인다
word_vectors.similar_by_word("love")
#result
[('suspenseful', 0.9094112515449524),
('commendable', 0.896395206451416),
('dreams', 0.891902506351471),
('dish', 0.8790621757507324),
('humor', 0.8635135889053345),
('echoes', 0.853840708732605),
('loved', 0.8497798442840576),
('newer', 0.8420393466949463),
('ned', 0.8417032957077026),
('worth', 0.8411469459533691)]
외부에서 미리 만들어진 워드 벡터 또한 존재하기 때문에 불러서 손쉽게 사용할 수 있다 아래에 내용은 GoogleNews 워드 벡터를 다운 받는 코드이다(리눅스 환경) 용량이 다소 큼을 주의하자
# Get Word Vector from Google
!wget -c "https://s3.amazonaws.com/dl4j-distribution/GoogleNews-vectors-negative300.bin.gz"
워드 벡터 로드하기
from gensim.models import KeyedVectors
word2vec_path = 'GoogleNews-vectors-negative300.bin.gz'
word2vec = KeyedVectors.load_word2vec_format(word2vec_path, binary=True, limit=1000000)
vector = word2vec['computer']
vector
#result
~
5.63964844e-02, 2.23632812e-01, -5.49316406e-02, 1.46484375e-01,
5.93261719e-02, -2.19726562e-01, 6.39648438e-02, 1.66015625e-02,
4.56542969e-02, 3.26171875e-01, -3.80859375e-01, 1.70898438e-01,
5.66406250e-02, -1.04492188e-01, 1.38671875e-01, -1.57226562e-01,
3.23486328e-03, -4.80957031e-02, -2.48046875e-01, -6.20117188e-02],
dtype=float32)
아까보다 훨씬 가까워보이는 단어들이 많은데, 저기에 hate 단어가 있는 것이 흥미롭다
애증의 관계를 표현하는 뜻의 hate가 아닐까 생각해본다
word2vec.similar_by_word("love")
#result
[('loved', 0.6907792091369629),
('adore', 0.6816873550415039),
('loves', 0.6618633270263672),
('passion', 0.6100709438323975),
('hate', 0.600395679473877),
('loving', 0.5886635780334473),
('affection', 0.5664337873458862),
('undying_love', 0.5547305345535278),
('absolutely_adore', 0.5536839962005615),
('adores', 0.5440906882286072)]
해당 단어의 벡터 전체를 임베딩 한다
vocab_size = 10000 # 어휘 사전 크기
word_vector_dim = 300 # 워드 벡터의 차원수
embedding_matrix = np.random.rand(vocab_size, word_vector_dim)
# embedding_matrix에 word2vec 워드벡터 단어를 카피
for i in range(4, vocab_size):
if index_to_word[i] in word2vec:
embedding_matrix[i] = word2vec[index_to_word[i]]
아까 단어와 인덱스만 매칭했던 것과는 달리 단어간의 관계가 고려되는 벡터 자체를 넣어주는 것이 핵심이다
from tensorflow.keras.initializers import Constant
vocab_size = 10000
word_vector_dim = 300
# model
model = tf.keras.Sequential()
model.add(tf.keras.layers.Embedding(
vocab_size, word_vector_dim,
embeddings_initializer=Constant(embedding_matrix), # 카피한 임베딩 활용
input_length=maxlen,
trainable=True, # True를 주면 fine tuning
))
model.add(tf.keras.layers.LSTM(512))
model.add(tf.keras.layers.Dense(512, activation='relu'))
model.add(tf.keras.layers.Dense(128, activation='relu'))
model.add(tf.keras.layers.Dense(32, activation='relu'))
model.add(tf.keras.layers.Dense(1, activation='sigmoid'))
model.summary()
model.compile(
optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy']
)
epochs=5
history = model.fit(
partial_x_train,
partial_y_train,
epochs=epochs,
batch_size=512,
validation_data=(x_val,y_val),
verbose=1
)
results = model.evaluate(x_test, y_test, verbose=2)
print(results)
#result
782/782 - 28s - loss: 0.3627 - accuracy: 0.8461
[0.3626789450645447, 0.8461199998855591]
정확도가 90까지 올라가지 못한 것이 아쉽지만 괜찮은 성능의 분류기를 직접 만들어볼 수 있었다
레이어를 어떻게 쌓아야할지에 대한 지식이 없는 상태에서 단어의 인덱스화, 또는 벡터화를 진행한 이후
텐서플로우를 활용해서 바닥부터 자연어 처리에 대해 경험하는 것이 신선한 경험이었고
이를 외부의 모델과 워드 벡터를 잘 불러와서 쓸 수 있다면 단 시간내로 90퍼 이상의 성능을 올릴 수
있을 거란 자신감이 들었다
네이버 영화 리뷰로 감성 분석을 시도한 것은 점수에 영향이 가는 프로젝트이므로 아직 블로그에
올리진 못했지만 이 역시 즐거운 경험이었으며 자연어처리를 좀 더 밑바닥부터 채워나가는 좋은 시간이었다