2019.06.15
출처 : https://wikidocs.net/book/2155
핵심키워드
- RNN
RNN을 이용한 언어 모델링(Language modeling using Recurrent Neural Networks)
앞서 언어 모델 챕터에서 n-gram을 이용한 언어 모델을 배운 적이 있다. 언어 모델을 사용해서 문장을 생성하는 작업을 할 수 있다. 당시 n-gram을 이용한 언어 모델은 여러가지 한계점이 있었고, 충분한 훈련 데이터와 적합한 설계가 뒤따른다면 n-gram을 이용한 언어 모델보다 성능적으로 개선된 것이 RNN을 이용한 언어 모델이다. 혼동하지 말아야할 것은 RNN은 새로운 언어 모델의 이름을 말하는 것이 아니다. RNN은 뉴럴 네트워크 중 하나이며 RNN을 이용해서 언어 모델링이 가능하다.
언어 모델은 다음 단어를 예측하는 일을 할 수 있다고 언급한 바 있다. RNN을 통해서 가장 간단한 모델로 다음 단어를 예측하려면 어떻게 설계하면 될까? 예를 들어서 ‘영웅은 죽지 않아요 너만 빼고’ 라는 문장이 있었다고 했을 때 훈련 데이터는 다음과 같이 설계될 수 있다.
위의 표는 훈련 데이터를 [현재 단어, 다음 단어]의 쌍(Pair)으로 구성한 모습을 보여준다. RNN은 현재 단어를 입력받고, 다음 단어를 예측해야 한다. 이를 그림으로 표현하면 다음과 같다.
이에 대한 RNN 설계를 한번 해보도록 하자. 우선 모든 단어에 대해서 원-핫 인코딩을 통해 원-핫 벡터를 만들고, 또한 임베딩 층(embedding layer)을 통해 임베딩 벡터를 만든다. 사실 보통의 경우에는 임베딩 벡터로 만들면 원-핫 벡터보다 차원이 작아진다는 장점이 있지만, 여기서는 단어의 개수가 적으므로 임베딩 벡터의 차원도 5로 가정한다. 아래의 임베딩 벡터들은 실제로 임베딩을 수행한 것이 아니라 저자가 임의로 임베딩 벡터를 만들었다고 가정하고 만든 랜덤값이다.
이번 실습은 실제로 모델을 설계해서 훈련하는 것이 아니라 모델링 이해를 위한 실습이다. 실제 실습은 이 다음에 진행한다.
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras.layers import SimpleRNN
tf.enable_eager_execution()train_X = [[0.1, 4.2, 1.5, 1.1, 2.8],[1.0, 3.1, 2.5, 0.7, 1.1],
[0.3, 2.1, 1.5, 2.1, 0.1], [2.2, 1.4, 0.5, 0.9, 1.1]]
print(np.shape(train_X))
(4, 5)
앞서 ‘영웅은’, ‘죽지’, ‘않아요’, ‘너만’이라는 단어들을 각 시점의 훈련 데이터인 X로 사용한다고 언급한 바 있다. 여기서는 RNN의 입력으로 임베딩 벡터(embedding)를 사용한다. 입력의 크기(shape)를 출력해보면 (4, 5)가 나온다. 이는 2차원 텐서에 해당되며 앞서 인공신경망 챕터에서 배운 대로 (input_length, input_dim)의 크기에 해당된다. 하지만 여기서 input_length는 곧 시점(time-step)이므로, 이를 (timesteps, input_dim)으로 생각할 수 있다.
이를 RNN에 넣어서 RNN이 어떤 결과를 리턴하는지를 확인해보자. 그런데 앞서 RNN은 2D 텐서가 아니라 3D 텐서를 입력을 받는다고 언급한 바 있다. 즉, 위에서 만든 2D텐서를 3D 텐서로 변경한다.
train_X = [[[0.1, 4.2, 1.5, 1.1, 2.8], [1.0, 3.1, 2.5, 0.7, 1.1],[0.3, 2.1, 1.5, 2.1, 0.1], [2.2, 1.4, 0.5, 0.9, 1.1]]]train_X = np.array(train_X, dtype=np.float32)
print(train_X.shape)
(1, 4, 5)
(batch_size, timesteps, input_dim)에 해당되는 (1, 4, 5)의 크기를 가지는 3D 텐서가 생성되었다. batch_size는 한 번에 RNN이 학습하는 데이터의 양을 설정하는데, 여기서는 사실 학습하는 데이터. 즉, 문장이 1개 밖에 없으므로 batch_size는 1이다. 이를 RNN에 넣어보고, RNN이 출력하는 은닉 상태를 살펴보도록 하자.
rnn = SimpleRNN(3, return_sequences=True, return_state=True)hidden_states, last_states = rnn(train_X)print('train_X : {}, shape: {}'.format(train_X, train_X.shape))
# 입력으로 사용한 훈련 데이터
print('hidden states : {}, shape : {}'.format(hidden_states, hidden_states.shape))
# 모든 time-step의 은닉 상태
print('last hidden states : {}, shape : {}'.format(last_states, last_states.shape))
# 마지막 은닉 상태train_X : [[[0.1 4.2 1.5 1.1 2.8]
[1. 3.1 2.5 0.7 1.1]
[0.3 2.1 1.5 2.1 0.1]
[2.2 1.4 0.5 0.9 1.1]]], shape: (1, 4, 5)
hidden states : [[[-0.91452026 -0.99837923 0.44184488]
[-0.98993003 -0.9733108 0.8201469 ]
[-0.7832301 -0.879311 -0.21982159]
[-0.9632263 -0.7593323 0.2865555 ]]], shape : (1, 4, 3)
last hidden states : [[-0.9632263 -0.7593323 0.2865555]], shape : (1, 3)
입력으로 사용한 훈련 데이터는 앞에서 봤듯이 (batch_size, timesteps, input_dim)에 해당하는 (1, 4, 5)의 크기를 가지는 3D 텐서이다. RNN은 이 크기의 입력을 받아서 return_sequences=True라고 설정해주었기 때문에 모든 시점(time-step)에 대해서 출력을 리턴한다. 이에 따라 출력은 (batch_size, timesteps, output_dim)에 해당하는 (1, 4, 3) 크기의 3D 텐서를 출력한다.
하지만 return_sequences=True를 선택해주지 않는다면, RNN은 오직 마지막 은닉 상태만 출력한다고 설명했다. 그래서 이번 실습에서는 마지막 은닉 상태도 따로 일부로 리턴을 받아서 크기를 확인해보려고 했다. return_state = True를 선택하면 RNN은 마지막 은닉 상태를 리턴한다. 마지막 은닉 상태의 크기를 보면 (1, 3)이다. 이는 (batch_size, output_dim)에 해당된다. 여기까지가 은닉층에 대한 설계이다. 이제 출력층에 대한 설계를 해야한다.
위의 그림은 출력층을 제대로 설계했을 때, 시점이 3일때의 동작을 보여준다. 궁극적으로 해야할 것은 실제값 y와 출력층이 만들어낸 예측값 y-hat에 대해서 손실 함수(loss function)을 통해서 학습하는 것이다. 여기서 실제값 y의 역할을 원-핫 벡터가 해야한다. 시점이 2일 때 실제값 y는 다음 단어에 해당되는 ‘너만’의 원-핫 벡터인 [0 0 0 1 0]이다.
앞선 설명에서 은닉층의 RNN 셀이 각각의 시점에 대해서 은닉 상태 벡터의 크기. 즉, output_dim이 3인 벡터를 만들어내는 것을 확인했다. 그런데 실제값 y인 원-핫 벡터는 차원이 5이다. 그렇다면, 예측값 또한 출력층을 통해 output_dim을 다시 5로 바꿔줄 필요가 있다. 그래야 손실 함수를 적용할 수 있기 때문이다. 이는 출력층에 model.add(Dense(5))를 통해 5개의 뉴런을 추가하면 해결된다.
앞서 셋 이상의 정답지 중에서 정답을 고르는 분류 문제의 경우 출력층의 활성화 함수로 소프트맥스 함수를 사용한다고 언급한 바 있다. 이 문제도 예측 벡터의 5개의 차원 중에서 어떤 값이 선택되느냐에 따라서 5개의 정답지 중 하나를 고르는 문제와 동일하다. 소프트맥스 함수를 지난 예측 벡터는 각각의 값은 0과 1 사이의 값이면서, 5개의 차원의 총 합은 1인 벡터로 바뀐다.
이 5개의 차원에서 각각의 값이 의미하는 바는 해당 위치의 값이 1일 확률이다. 예를 들어 [0.7, 0.05, 0.05, 0.1, 0.1]과 같은 예측 벡터가 나왔다면 0.7이 가장 높은 값이므로 원-핫 벡터가 [1 0 0 0 0]인 ‘영웅’이라는 단어가 정답이라고 70%의 확신을 가지고 예측했다는 의미이다.
t가 3일 때는 실제값 y의 원-핫 벡터가 [0 0 0 1 0] 이므로, 제대로 된 예측이라고 함은 예측 벡터의 네번째 차원의 값이 가장 높은 값이 나오고, 나머지 차원의 값은 상대적으로 낮아야 한다. 그리고 이러한 오차(loss)의 갭을 줄이기 위해 오차 함수로 크로스 엔트로피 함수를 사용한다.
이제 이러한 이해를 가지고 실제 코드를 작성하여 실습을 해보도록 한다. 아래의 실습에서는 이해를 돕기 위해 아주 적은 양의 데이터를 가지고 진행하므로 별도 테스트 데이터를 분리하지 않는다.
1) 다음 단어 예측해보기
RNN을 통해서 주어진 문장을 학습하고, 다음 단어를 예측하는 인공 신경망 모델을 만들어보도록 한다. 아래의 코드에서 text에 한국어 문장 대신 영어 문장을 저장하고 사용해도 모델은 작동한다.
text="나랑 점심 먹으러 갈래 메뉴는 햄버거 점심 메뉴 좋지"from keras_preprocessing.text import Tokenizer
t = Tokenizer()
t.fit_on_texts([text])
encoded = t.texts_to_sequences([text])[0]
# [0]을 해주지 않으면 [[contents]]와 같은 리스트 안의 리스트 형태로 저장 됨.
# [0]을 해주면 [contents]와 같은 하나의 리스트로 저장됨.
우선 토큰화와 정수 인코딩 과정을 거친다
vocab_size = len(t.word_index) + 1
# 케라스 토크나이저의 정수 인코딩은 인덱스가 1부터 시작하지만,
# 케라스 원-핫 인코딩에서 배열의 인덱스가 0부터 시작하기 때문에
# 배열의 크기를 실제 단어 집합의 크기보다 +1로 생성해야하므로 미리 +1 선언
print('단어 집합의 크기 : %d' % vocab_size)
단어 집합의 크기 : 9
실제 단어의 개수는 8이지만, 뒤에서 필요한 배열의 크기는 9이므로 +1을 해주었다.
print(t.word_index)
{'점심': 1, '나랑': 2, '먹으러': 3, '갈래': 4, '메뉴는': 5, '햄버거': 6, '메뉴': 7, '좋지': 8}
문장에 점심이라는 단어가 두 번 등장했고, 나머지 단어는 전부 1번씩만 등장했기 때문에 점심에 1번 인덱스가 할당된다.
sequences = list()
for c in range(1, len(encoded)):
sequence = encoded[c-1:c+1]
# 단어를 두개씩 묶어서 저장해준다.
# 이는 X와 Y의 관계를 구성하기 위함이다.
sequences.append(sequence)
print('단어 묶음의 개수: %d' % len(sequences))
이제 문장에서 두 개의 단어씩 묶는 작업을 해준다. 여기서는 문장의 Bigram을 추출한다고 봐도 무방하다.
단어 묶음의 개수: 8
단어 묶음의 개수는 총 8개가 나온다. 한 번 출력해보도록 한다.
print(sequences)[[2, 1], [1, 3], [3, 4], [4, 5], [5, 6], [6, 1], [1, 7], [7, 8]]
# 위의 결과는 아래와 같다. 첫번째 열이 X가 되고, 두번째 열이 예측해야할 다음 단어인 Y가 될 것이다.# [나랑, 점심], # [점심, 먹으러], # [먹으러, 갈래], # [갈래, 메뉴는], # [메뉴는, 햄버거] # [햄버거, 점심] # [점심, 메뉴] # [메뉴, 좋지]
해당 단어의 묶음에서 첫번째 열을 X, 두번째 열을 y로 저장한다면 X를 현재 등장한 단어, y를 다음에 등장할 단어로 하여 훈련 데이터로 사용할 수 있다.
import numpy as np
X, y = zip(*sequences) # 첫번째 열이 X, 두번째 열이 y가 됨.
X=np.array(X) # 타입을 배열로 변환
y=np.array(y) # 타입을 배열로 변환
각각 첫번째 열과, 두번째 열을 X와 y에 저장하는 코드이다.
from keras.utils import to_categorical
y = to_categorical(y, num_classes=vocab_size) # 원 핫 인코딩
print(y)
y에 한해서 원-핫 인코딩을 수행한다. 아래는 y에 해당되는 [1, 3, 4, 5, 6, 1, 7, 8]이 원-핫 인코딩된 결과를 보여준다.
[[0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 1. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 1. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 1. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 1. 0. 0.]
[0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 1. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 1.]]
이제 X와 y를 가지고, 현재 단어가 주어지면 다음 단어를 예측하는 모델을 만든다.
from keras.layers import Embedding, Dense, SimpleRNN
from keras.models import Sequential
model = Sequential()
model.add(Embedding(vocab_size, 9, input_length=1))
# 단어 집합의 크기는 9. 임베딩 벡터의 크기는 9. 각 sample의 길이는 단어 한 개이므로 길이는 1.
model.add(SimpleRNN(9))
# RNN의 결과값으로 나오는 벡터의 차원 또한 9로 한다. 더 크게 해주어도 상관은 없음.
model.add(Dense(vocab_size, activation='softmax'))
# 출력층을 지나서 나오는 벡터의 크기도 9로 한다.
여기서 주의할 점은 SimpleRNN에 하나의 값만을 인자로 줄 때 이 값은 시점(time-step)이 아니라, hidden_size의 값. 즉, output_dim을 정의해준 숫자이다.
model.compile(loss='categorical_crossentropy',optimizer='adam',
metrics=['accuracy'])
model.fit(X, y, epochs=500, verbose=2)
다수의 선택지 중에서 하나를 고르는 문제이므로 categorical_crossentropy를 사용하며, 총 500번 학습한다.
Epoch 1/500
- 1s - loss: 2.1968 - acc: 0.0000e+00
Epoch 2/500
- 0s - loss: 2.1939 - acc: 0.0000e+00
...
중략
...
Epoch 499/500
- 0s - loss: 0.3358 - acc: 0.8750
Epoch 500/500
- 0s - loss: 0.3348 - acc: 0.8750
여기서 더 학습을 해도 정확도는 87%를 넘어가지 않는데, 그 이유는 위에서 선언한 문장에서 ‘점심’ 다음에 ‘먹으러’라는 단어가 등장하기도 했고, ‘점심’ 다음에 ‘메뉴’라는 단어가 등장한 케이스도 있다. 이 케이스가 정확하게 각각 한 번으로 동이랗게 등장했기 때문에 충분하지 않은 데이터로 인한 애매함(ambiguity)으로 기계가 헷갈리고 있기 때문이다.
그럼 이제 입력한 단어에 대해서 다음 단어를 정상적으로 예측하는지 직접 확인해보도록 한다.
print(t.word_index.items())dict_items([('점심', 1), ('나랑', 2), ('먹으러', 3), ('갈래', 4), ('메뉴는', 5), ('햄버거', 6), ('메뉴', 7), ('좋지', 8)])
다음과 같이 word_index.items()는 key와 value의 쌍을 튜플로 묶은 값을 dict_items 객체로 리턴한다. 그리고 이를 이용하여 다음 단어를 출력하는 함수를 작성해본다.
def predict_next_word(model, t, current_word):
# 모델, 토크나이저, 현재 단어를 입력으로 받음
encoded = t.texts_to_sequences([current_word])[0]
# 현재 단어에 대한 정수 인코딩
encoded = np.array(encoded)
result = model.predict_classes(encoded, verbose=0)
# 입력한 X(현대 단어)에 대해서 Y를 예측하고 Y(예측한 단어)를
# result에 저장
for word, index in t.word_index.items():
# 위에서 실습한 것처럼 단어와 인덱스를 리턴
if index == result:
# 만약 예측한 단어와 인덱스와 동일한 단어가 있다면
return word
# 그 단어를 출력print(predict_next_word(model, t, '먹으러'))
갈래
‘먹으러’라는 단어에 대해서 정상적으로 ‘갈래’를 예측했음을 확인할 수 있다. 이제는 주어진 단어로부터 문장을 생성하는 함수를 만들어보도록 한다. 방금 위에서 했던 함수를 for 문과 조합하여 반복하도록 만들면 된다.
def sentence_generation(model, t, current_word, n):
# 모델, 토크나이저, 현재 단어, 반복할 횟수
init_word = current_word
# 처음 들어온 단어도 마지막에 같이 출력하기 위해 저장해놓음
sentence = ''
for _ in range(n):
encoded = t.texts_to_sequences([current_word])[0]
# 현재 단어에 대한 정수 인코딩
encoded = np.array(encoded)
# 현재 단어에 대한 정수 인코딩
result = model.predict_classes(encoded, verbose=0)
# 입력한 X(현재 단어)에 대해서 Y를 예측하고 Y(예측한 단어)를
# result에 저장
for word, index in t.word_index.items():
if index == result:
# 만약 예측한 단어와 인덱스가 동일한 단어가 있다면
break
# 해당 단어가 예측 단어이므로 break
current_word = word
# 예측 단어를 현재 단어로 변경
sentence = sentence + ' '+ word
# 예측 단어를 문장에 저장
# 전체 반복
sentence = init_word + sentence
return sentenceprint(sentence_generation(model, t, '먹으러', 6))
먹으러 갈래 메뉴는 햄버거 점심 메뉴 좋지
2. 문맥을 반영해서 다음 단어 예측해보기
앞선 모델은 훈련 데이터에 ‘점심’ 다음에 ‘먹으러’ 와 ‘메뉴’라는 단어가 한 번씩 나옴에 따라 어떤 단어를 선택할지 헷갈리는 모습을 보여주었다. 하지만 다음 단어의 등장 횟수가 같다고해서 헷갈린다는 것은 해당 모델이 문맥을 전혀 반영하지 못하는 모델이라는 것을 의미한다.
예를 들어서 ‘경마장에 있는 말이 뛰고 있다’와 ‘그의 말이 법이다’와 ‘가는 말이 고와야 오는 말이 곱다’라는 세 가지 문장이 있다고 해보자. 이 세 문장을 위에서 만든 모델에 학습시킨다면 모델은 앞에 어떤 문맥이 있었는지는 상관없이 ‘말이’라는 단어 뒤에 다음 단어를 예측하는 문제를 냈을 때 ‘뛰고’, ‘법이다’, ‘고와야’, ‘곱다’ 네 가지 단어 중에서 헷갈리기 시작한다. 위의 모델은 오직 현재의 단어를 기준으로 다음 단어를 예측하기 때문이다
이를 위한 대안은 모델이 문맥을 학습할 수 있도록 앞의 단어들도 함께 학습시키는 것이다. 즉, 이번에 만드는 모델은 이전 모델처럼 현재 단어와 다음 단어의 쌍(pari)을 X와 y로 훈련시키는 것이 아니라 앞에 등장한 모든 단어의 시퀀스를 X로 학습시킨다.
그럼 직접 모델을 만들어보고 정말로 문맥을 반영할 수 있는지 확인해보자.
text = """경마장에 있는 말이 뛰고 있다\n
그의 말이 법이다\n
가는 말이 고와야 오는 말이 곱다\n"""
우선 에제로 설명 든 한국어 문장을 선언한다.
from keras_preprocessing.text import Tokenizer
t = Tokenizer()
t.fit_on_texts([text])
encoded = t.texts_to_sequences([text])[0]
토큰화와 정수 인코딩 과정을 거친다.
vocab_size = len(t.word_index)+1
# 케라스 토크나이저의 정수 인코딩은 인덱스가 1부터 시작하지만,
# 케라스 원-핫 인코딩에서 배열의 인덱스가 0부터 시작하기 때문에
# 배열의 크기를 실제 단어 집합의 크기보다 +1로 생성해야하므로 미리 +1 선언print('단어 집합의 크기: %d' % vocab_size)
단어 집합의 크기: 12print(t.word_index)
{'말이': 1, '경마장에': 2, '있는': 3, '뛰고': 4, '있다': 5, '그의': 6, '법이다': 7, '가는': 8, '고와야': 9, '오는': 10, '곱다': 11}
여기까지는 앞선 실습과 코드가 동일하다. 하지만 훈련 데이터를 만드는 아래부터는 코드가 달라진다.
sequences = list()
for line in text.split('\n'): # \n을 기준으로 문장 토큰화
encoded = t.texts_to_sequences([line])[0]
for i in range(1, len(encoded)):
sequence = encoded[:i+1]
sequences.append(sequence)
print('훈련 데이터의 개수: %d' % len(sequences))
훈련 데이터의 개수: 11print(sequences)
[[2, 3], [2, 3, 1], [2, 3, 1, 4], [2, 3, 1, 4, 5], [6, 1], [6, 1, 7], [8, 1], [8, 1, 9], [8, 1, 9, 10], [8, 1, 9, 10, 1], [8, 1, 9, 10, 1, 11]]
위의 데이터는 아직 X와 y가 구분되지 않은 훈련 데이터이다. [2, 3]은 [경마장에, 있는]에 해당되며 [2, 3, 1]은 [경마장에, 있는, 말이]에 해당된다. 모든 훈련 데이터에 대해서 맨 우측에 있는 단어에 대해서만 y로 분리하면 X와 y의 쌍(pari)이 된다.
X와 y를 분리하기 전에 해야할 일은 모든 데이터의 길이를 맞추는 것이다. 모든 데이터의 길이를 맞출 때는 보통 가장 긴 데이터를 기준으로 길이를 맞춘다. 현재 육안으로 봤을 때, 가장 데이터의 길이가 긴 것은 [8, 1, 9, 10, 1, 11]이다. 이를 코드로 구하는 방법은 다음과 같다.
print(max(len(l) for l in sequences))
# 모든 데이터에서 길이가 가장 긴 데이터의 길이 출력
6
모든 데이터에서 길이가 가장 긴 데이터의 길이가 6임을 확인하였다. 이제 모든 데이터에 대해서 길이 6으로 길이를 맞춰준다.
from keras.preprocessing.sequence import pad_sequences
sequences = pad_sequences(sequences, maxlen=6, padding='pre')
케라스의 도구인 pad_sequences()는 모든 데이터에 대해서 0을 추가하여 길이를 맞춰준다. maxlen의 값으로 6을 주면 모든 데이터의 길이를 6으로 맞춰주며, padding의 인자로 ‘pre’를 주면 길이가 6보다 짧은 데이터의 앞을 0으로 채운다.
print(sequences)
[[ 0 0 0 0 2 3]
[ 0 0 0 2 3 1]
[ 0 0 2 3 1 4]
[ 0 2 3 1 4 5]
[ 0 0 0 0 6 1]
[ 0 0 0 6 1 7]
[ 0 0 0 0 8 1]
[ 0 0 0 8 1 9]
[ 0 0 8 1 9 10]
[ 0 8 1 9 10 1]
[ 8 1 9 10 1 11]]
길이가 6보다 짧은 모든 데이터에 대해서 앞에 0을 채움에 따라, 모든 데이터의 길이가 6이되는 것을 확인할 수 있다. 이제 각 데이터의 마지막 단어만 y로 분리하면 X와 y를 분리할 수 있다. X와 y의 분리는 다음과 같이 Numpy를 이용해 가능하다.
import numpy as np
sequences = np.array(sequences)
X = sequences[:, :-1]
y = sequences[:, -1]
# 리스트의 마지막 열을 제외하고 저장한 것은 X
# 리스트의 마지막 열만 저장한 것은 y
분리된 X와 y는 다음과 같다.
print(X)
[[ 0 0 0 0 2]
[ 0 0 0 2 3]
[ 0 0 2 3 1]
[ 0 2 3 1 4]
[ 0 0 0 0 6]
[ 0 0 0 6 1]
[ 0 0 0 0 8]
[ 0 0 0 8 1]
[ 0 0 8 1 9]
[ 0 8 1 9 10]
[ 8 1 9 10 1]]print(y)
[ 3 1 4 5 1 7 1 9 10 1 11]
X와 y가 정상적으로 분리된 것을 알 수 있다. 이제 RNN 모델에 훈련 데이터를 훈련 시키기 전에 y에 대해서 원-핫 인코딩을 수행한다.
from keras.utils import to_categorical
y = to_categorical(y, num_classes=vocab_size)
# y에 대한 원-핫 인코딩 수행print(y)
[[0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]
[0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]
[0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
[0. 1. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]]
정상적으로 원-핫 인코딩이 수행된 것을 볼 수 있다. 이제 RNN 모델에 데이터를 훈련 시킨다.
from keras.layers import Embedding, Dense, SimpleRNN
from keras.models import Sequentialmodel = Sequential()
model.add(Embedding(vocab_size, 10, input_length=5))
# y를 제거하였으므로 이제 x의 길이는 5model.add(SimpleRNN(32))
model.add(Dense(vocab_size, activation='softmax'))
model.compile(loss='categorical_crossentropy', optimizer='adam',
metrics=['accuracy'])
model.fit(X, y, epochs=200, verbose=2)Epoch 1/200
- 0s - loss: 2.4797 - acc: 0.0000e+00
Epoch 2/200
- 0s - loss: 2.4670 - acc: 0.0000e+00
Epoch 3/200
- 0s - loss: 2.4544 - acc: 0.1818
...
Epoch 123/200
- 0s - loss: 0.4194 - acc: 0.9091
Epoch 124/200
- 0s - loss: 0.4115 - acc: 1.0000
...
Epoch 199/200
- 0s - loss: 0.1033 - acc: 1.0000
Epoch 200/200
- 0s - loss: 0.1016 - acc: 1.0000
앞에서 처음 만든 RNN 모델과 달리 중간부터 정확도가 100%가 나오는 것을 확인할 수 있다. 이제 모델이 헷갈리 여지를 제거해주었기 때문이다. 모델이 정확하게 예측하고 있는지 문장을 생성하는 함수를 만들어서 실제로 출력해보도록 한다.
def sentence_generation(model, t, current_word, n):
# 모델, 토크나이저, 현재 단어, 반복 횟수
init_word = current_word
# 처음 들어온 단어도 마지막에 출력하기 위해 저장
sentence = ''
for _ in range(n):
encoded = t.texts_to_sequences([current_word])[0]
# 현재 단어에 대한 정수 인코딩
encoded = pad_sequences([encoded], maxlen=5, padding='pre')
# 데이터에 대한 패딩
result = model.predict_classes(encoded, verbose=0)
# 입력한 X(현재 단어)에 대해서 Y를 입력하고
# Y(예측한 단어)를 result에 저장.
for word, index in t.word_index.items():
if index == result:
break
current_word = current_word + ' ' + word
# 현재 단어 + ' ' + 예측 단어를 현재 단어로 변경
sentence = sentence + ' ' + word
# 전체 반복
sentence = init_word + sentence
return sentence
이제 첫 단어로부터 마지막 문장까지 생성해내는 함수를 만들었다.
print(sentence_generation(model, t, '경마장에', 4))
# '경마장에' 라는 단어 뒤에는 총 4개의 단어가 있으므로 4번 예측경마장에 있는 말이 뛰고 있다print(sentence_generation(model, t, '그의', 2))
그의 말이 법이다print(sentence_generation(model, t, '가는', 5))
가는 말이 고와야 오는 말이 곱다
이제 앞의 문맥을 기준으로 ‘말이’ 라는 단어 다음에 나올 단어를 기존의 훈련 데이터와 일치하게 예측함을 보여준다.
이 모델은 충분한 훈련 데이터를 갖고 있지 못하므로 위에서 문장의 길이에 맞게 적절학 예측해야하는 횟수 4, 2, 5를 각각 인자값으로 주었다. 이 이상의 숫자를 주면 기계는 ‘있다’, ‘법이다’ , ‘곱다’ 다음에 나오는 단어가 무엇인지 배운 적이 없으므로 임의로 예측을 하기 시작한다.